Wheels 4.1.1 is out — a CLI security fix and the 4.1.0 papercuts
Wheels 4.1.1 is out. It’s a point release, so there are no new headline features. It does three things you’ll want: it closes a security issue in the CLI, it fixes the problems people hit after upgrading to 4.1.0, and it’s the first release we held until every test on every engine and database passed, checked test by test rather than job by job.
Full notes: GitHub Release v4.1.1 · CHANGELOG · Security advisory GHSA-x3cm-2j3q-jgg4
The security fix: the CLI now checks which server it’s talking to
Commands like wheels migrate, wheels reload, wheels console and wheels test talk to your app’s running server over HTTP. Up to 4.1.0 the CLI trusted whatever answered on the project’s port, or on a common development port such as 8080, 60000, 3000 or 8500. If a different application was listening there, the CLI could send it state-changing commands, console code, and your app’s reload password.
In 4.1.1:
- State-changing commands need this project’s own server, the one
wheels start(orwheels engines rustcfml start) launched. The CLI checks that the server is the registered process, that it’s the only thing listening on its port, and, on every request, that the connection is to that process before sending anything. Otherwise it refuses and names the port. - The reload password moves out of the URL into an
X-Wheels-Reload-Passwordrequest header, so it stays out of access logs and proxy logs. consolenever falls back to probing ports, and read-only commands that end up on a server they couldn’t verify now tell you so. SetWHEELS_SERVER_FALLBACK=falseto turn that read-only fallback off entirely.
It’s rated medium, and it’s local: the other application has to be running on your machine. Details, affected versions and workarounds are in the advisory.
4.1.1 also fixes an open redirect: redirectTo() and startFormTag()’s CSRF check accepted a URL such as https://your-host:x@evil.com/. Browsers read the part before the @ as credentials, so the redirect, and the CSRF token, went to another host.
One manual step for existing apps: copy the X-Wheels-Reload-Password header block from the current public/Application.cfc template into the top of onRequestStart() in your own public/Application.cfc, before the reload lock. Until you do, the CLI falls back to the query string, only for your project’s own verified server, and prints a notice.
Adobe ColdFusion: the first request after a restart works
On Adobe ColdFusion 2023 and 2025, the first request after every server start could return HTTP 500. Health checks usually hid it, and load-balanced deployments didn’t: each new instance served a 500 before it warmed up. Three separate causes, all fixed:
- an Adobe engine bug (
EmptyStackExceptioninpopSuperScope) hit while creating an internal framework component duringonApplicationStart; - Adobe 2025’s startup notice when the optional
graphqlclientpackage isn’t installed, which reached the app’sonError; request.wheelskeys thatonErrorcould leave half-initialized.
A new CI smoke test boots a cold server with the health check disabled, so this can’t come back quietly.
Migrations against a second database no longer stall
If a migration’s up() did its work with raw queryExecute() or cfquery, for example DDL against a second datasource, and then called announce(), 4.1.0 decided it was a placeholder and never recorded it. The migration ran again on every migrate. When the re-run failed, say on CREATE TABLE for a table that already exists, every migration queued behind it stopped.
4.1.1 records every migration step that completes, as 4.0.x did. The only exception is a migration that still has the inherited NOT IMPLEMENTED placeholder up()/down(), and the migrator now logs a warning when it skips one. After upgrading, a migration 4.1.0 left unrecorded runs once more and is then recorded. If that re-run fails because its work already exists, mark it applied with wheels migrate pretend <version> --yes.
Failed migrations during application-start auto-migrate are no longer silent either: they’re written to wheels.log.
Everything else
Database fixes
- Oracle works across engines:
insertAll()on Lucee and BoxLang, datetime columns returned as real dates on BoxLang and Adobe, and timestamp handling on Adobe. - SQL Server:
create()with an explicit primary key no longer fails on identity columns. - H2:
offset()withoutlimit()returns rows in the right order. - Table names with
$work again. Thanks to @mikegrogan for #3669.
Model and query builder
- The query builder’s
offset()is now actually applied. updateAll(instantiate=true, validate=false)skips validations as documented.upsertAll()keeps an existing row’screatedAt.- Compound
condition/unlessvalidation expressions (&&,||, a leading!) work, with CFML precedence. - A
transaction="none"call that throws no longer disables transactions for the rest of the request. whereclauses no longer split onAND/ORinside an identifier.
Routing and views
paginationLinks()works on wildcard-routedindexactions. Thanks to @mlibbe for #3638.- Other pagination fixes:
paramsare kept whenpageNumberAsParam=false, andprependOnFirst=falseno longer mangles the first link. - Typed route constraints apply to every optional-segment variant, and route variables aren’t double-encoded when URL rewriting is off.
linkTo(method="delete")emitsrel="nofollow", spelled correctly.singularize()leavesapi,safariandspaghettialone.
CLI and MCP
- Arguments are validated strictly. Unknown named arguments, bad boolean values (
--force=yes) and out-of-range choices now fail instead of being guessed at. wheels test:- it exits non-zero when nothing ran;
--test-timeoutsets the run’s time budget;- it finds your server when the directory name differs from
lucee.json; - an immediate re-run after a timeout no longer collides with the abandoned run.
wheels stopforwards--name,--configand--all.- Generators:
generate model --belongsToadds the foreign-key column; plain controllers get plain views;wheels destroywrites a migration that runs. - MCP:
deployis no longer exposed as an MCP tool.
Performance
- Flash locking is per session instead of application-wide, so users no longer queue behind each other on every request.
Startup errors
- When Wheels fails to initialize, the real root cause now goes to
wheels.loginstead of the error page pointing at a log entry that didn’t exist.
What “every test passed” means this time
We’ve always run the full engine × database matrix: Lucee 6 and 7, Adobe 2023 and 2025, and BoxLang, against SQLite, H2, MySQL, PostgreSQL, SQL Server, CockroachDB and Oracle. For 4.1.1 we tightened what “green” means.
- Oracle no longer passes by default. Its legs used to be allowed to fail, and one could look green while running zero tests. They now fail the build like any other database, and all five ran more than 5,800 tests each.
- Release PRs run the full matrix. A release PR into
mainnow runs the whole matrix automatically, and a readiness check blocks it until every open issue and PR is resolved or explicitly deferred. - Every result was audited, test by test. Before we released, we reconciled each leg’s raw results. One CockroachDB leg failed a rate-limiter test that happened to run across a minute boundary (16:16:59.948 to 16:17:00.010). We confirmed that was the cause, re-ran that leg on identical code, and scheduled the test fix for 4.1.2 instead of calling it a flake and moving on.
Upgrading
brew upgrade wheels # macOS / Linux (Homebrew)
scoop update wheels # Windows
sudo apt update && sudo apt upgrade wheels # Debian / Ubuntu
sudo dnf upgrade wheels # RHEL / Rocky / Fedora
wheels upgrade check
wheels upgrade apply
The CLI now runs on LuCLI 0.6.2.1 on every channel.
Three things to check:
- Copy the reload-password header block into your
public/Application.cfc, as described above. - Scripts that pass loose CLI flags (
--force=yes, unknown options) now fail; switch them totrue/falseand remove unknown flags. - Multi-database migrations: after upgrading, watch for one extra run of any migration 4.1.0 left unrecorded, and use
wheels migrate pretendif one fails.
Known issue (apt): the Debian/Ubuntu wheels launcher uses JAVA_HOME even when it points to a JDK older than 21, and LuCLI needs Java 21. If wheels fails with UnsupportedClassVersionError, run unset JAVA_HOME or point it at a Java 21 JDK. The launcher fix (#3766) is merged and ships in 4.1.2.
Thanks
Thanks to @mikegrogan and @mlibbe for the reports above. Thanks to everyone who told us what broke after 4.1.0. And thanks to the RustCFML maintainers, who fixed the include-exception bug we reported (RustCFML#431) the same day, so our experimental RustCFML lane now runs with no expected failures.
If something in 4.1.1 doesn’t behave the way this post says, file it.
Comments