3.0.1
The audit release after the first product release. Update — 3.0.0 shipped the two editions and did not enforce them.
The editions, actually enforced
Every Enterprise feature was live on a Community install. All forty-six of them. There was no edition check anywhere in the request path — not at registration, not in the per-account feature list, not in the middleware — and the installer writes every module into the enabled list, so a Community download came up with webmail, email accounts, FTP, PostgreSQL, containers, the web terminal and the firewall.
The split now comes from one list, checked against the catalogue the website publishes, and applies in three places: an Enterprise module is never registered on Community, so it is a 404 before any handler; the per-account feature list is filtered, which is what the sidebar, the dashboard and every template read; and the five Enterprise pages that live inside Community modules are refused in the middleware.
Community could not enter a licence key. The only licence page in the product is on port 2087 — the port Community closes. The one edition that needs to buy its way out was expected to ssh in and run a command. There is a page on the hosting panel now, and it disappears the moment a key is present.
It would not have disappeared anyway: the key was read once at startup and never again, despite a comment saying otherwise, so a key entered anywhere changed nothing until a restart.
Community could not remove a PHP version. The route existed and nothing in the product ever called it. The install control was drawn on both editions and refused on one — a button that is always an error. Both are Community controls now, and on Enterprise they belong to the administrator.
Activating Enterprise installed the wrong database. It asked for the extra that installs MariaDB, which Community already has, and not the one that installs PostgreSQL — which is what the five Enterprise PostgreSQL pages sit in front of.
Security
-
A new account's password reached root SQL and a root shell unescaped. Three paths, one input, and the only check on a password arriving through the API is that it is not empty.
IDENTIFIED BY '<password>'was interpolated raw into amysql -erunning as root, so a quote in a password ran the rest as the database superuser. The PostgreSQL role was created throughsu - postgres -c, which hands its argument to a shell before psql sees it, so$(...)in a password executed. And the admin API's document root reached an UPDATE with no escaping and a validation that rejects only... -
The billing panel's "Login to Panel" was a credential replay. It auto-submitted an administrator's username and password at the login form, which the panel refused — correctly, and the refusal was the reported bug. Making it work by exempting the login form would have handed every web page a login-CSRF primitive: a password proves who is signing in, never that this browser's owner asked to. The hand-off carries a one-time ticket now — minted server-to-server against the token the billing module already holds, valid a minute, spendable once, bound to that account.
-
Origin: nullwas accepted. It fell through to the Referer check, and a request with neither passed the origin check entirely. Any page can manufacture that with a sandboxed iframe. -
A live GitHub token was served to every reader of the documentation. Hardcoded in a banner component that is registered in the MDX components and therefore shipped in the bundle.
Fixed
-
An update from 2.1.30 to 3.0.0 never arrived. The updater walked the whole numeric space between the two versions — 998,970 of them — and every one with no script on disk fell through to a mirror fetch with a thirty-second timeout. It did not fail; it sat in the 2.x range for days, and the log stopped at the last script that existed.
-
A server on a single-digit patch had stopped updating. Versions were compared as strings, so
2.1.9was newer than2.1.30. -
A stray bracket in a zone file hid every record below it. A
(inside a quoted TXT value counted as the start of a multi-line record, and the merge ran to the end of the file. A zone with fifty names showed one, and the records that vanished were still being served. On a large zone it was also minutes of CPU. -
An interrupted
domains-addleft the domain half-made — the row and the document root with no site behind either, and the panel then refused the name and offered no page to remove it. A cancelled request is a failure and now unwinds. -
The installer never showed the hosting panel's password. It printed the address of one panel above the credentials of the other, so a fresh Community install ended with nobody able to sign in to the panel that is the whole product on that edition.
-
The forbidden-domain list had never been read. The check looked for a file relative to the current directory while naming the real path in a variable it did not use.
-
Thirty-six conditions returned failure when they succeeded. A pipeline ending in
grep -qreports 141 underpipefailonce grep exits and the writer is killed — so a match read as a failure, on a busy server and not on a quiet one. One of them had been hiding a second bug: no alias has ever been granted the right to send as itself, which is the sharedsales@case the sender map was written for.
Documentation
Forty-three pages that had routes and no documentation. The editions and their
limits where a reader looks for them. The CLI reference regenerated from the
scripts rather than a snapshot listing sixteen commands that no longer exist.
update.md rewritten around how updating actually works. Thirty-odd broken
links.
Three things that were documented and wrong: the upgrade guide sent people to a page on the port Community closes, the expiry article said an expired licence keeps every feature when it falls back to Community outright, and the feature table listed 73 of the 140 features.
Notes
Nothing to migrate. The update script re-checks what 3.0.0's did and adds one thing: on a server with no licence key it says, out loud, that it is now Community and what that means.
Most of this was found by using the product, not by reading it. Nearly every fault here had a comment beside it describing what the code was supposed to do — "on loopback only", "left stopped", "read on each call rather than cached at startup". None of those was true, and none was found by reading them.