wheels-cli: Wheels CLI can send credentials and state-changing commands to a local server that isn't the current project's
What fixes it
- 4.1.1wheels-cli, otherGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
Several Wheels CLI commands talk to a local development server over HTTP but never verify that the server answering belongs to the current project. When a developer runs more than one Wheels app on the same machine (common, because every app defaults to port 8080), or when an unrelated process listens on a port the CLI probes, the CLI can:
- send a project's reload password to a server that isn't the project's own (
wheels reload, and the password is placed in the request URL); - send code for evaluation, together with the reload password, to a server that isn't the project's own (
wheels console, which also falls back to probing common ports when the project's server is down); - run state-changing operations against another application's server and database (
wheels migrate latest|up|down,wheels seed,wheels db reset,wheels jobs work), when another app is serving on the port this project declares.
Read-only commands (migrate info, db status/version, routes, info) can likewise report another app's state as the current project's.
Impact
Credential exposure (the reload password) and unintended code evaluation or data changes on a different local application. Exploitation requires a process listening on a local port the CLI targets; no remote network access is involved.
Affected
Wheels CLI through 4.1.0 (the wheels CLI and the public/Application.cfc app template).
Patches
Fixed in Wheels 4.1.1 (wheels CLI and the application template). Upgrade the CLI, and copy the header-mapping block at the top of onRequestStart() from the current template into your own public/Application.cfc.
With the fix, the CLI sends commands that change state or evaluate code, and the reload password, only to this project's own development server. This covers migrate write actions, seed, db reset, reload, console, jobs work, generate admin, coverage, test and browser test. The server is identified by all of the following:
- Registration. It is the server this project started (
wheels start, orwheels engines rustcfml start), recorded against this project's directory. - Process identity. The recorded process is that server: a Lucee server whose command line names this project's server directory, or a managed RustCFML binary serving this project's
publicdirectory. A stale or reused process ID is rejected. - Listener. Every process listening on the server's port is that process.
- Connection. For every request, the CLI first confirms that the server process holds the accepted end of the exact connection the request will use, and only then writes anything on it. A connection accepted by any other program sends nothing, whoever owns that program.
In addition:
- Requests go to the address the server actually binds (its loopback address when it binds a wildcard, or its specific address), never to whatever
localhostresolves to. - Responses are read strictly: a truncated or malformed response is an error, never a partial result that could read as success.
- If ownership cannot be established, the command refuses and names the port.
wheels consoleno longer probes other ports.- The CLI sends the reload password in an
X-Wheels-Reload-Passwordrequest header instead of the URL query string, so it stays out of access and proxy logs. The updatedpublic/Application.cfcaccepts the header and still accepts?password=. A password containing characters an HTTP header cannot carry (anything outside printable ASCII) is sent as?password=instead, still only to the verified server, with a notice; the password may then appear in that server's own access log. - Read-only commands (
routes,info,migrate info/doctor,db status/version,jobs status) may still use a server they cannot verify. They carry no secret and change nothing, and they print that the server is not verified as this project's. .envparsing no longer mistakes keys such asDB_PORTorMY_RELOAD_PASSWORDfor the app's port or reload password.
Workarounds
Until you can upgrade:
- Give each project a unique port in
lucee.json, and don't run other servers on it. - Before running state-changing CLI commands, stop other local development servers that might be listening on the same port or on the common development ports (8080, 60000, 3000, 8500).
- Avoid
wheels consoleandwheels reloadon machines where other users run servers.
Limitations
- Operating-system introspection is required. Verification reads socket ownership from
/proc(Linux),lsof(macOS) ornetstat(Windows). Where that isn't available or allowed, the CLI refuses rather than sending, so the command fails but nothing leaks. - Other servers run by the same OS user are covered; a malicious process run by that user is not. Other development servers run by the same user (another project, a leftover or crashed server, or anything else listening on the same port) are exactly what this fix handles, and the CLI no longer sends them anything. A *malicious* process running as the developer's own OS user is outside the boundary: it can already read the project's reload password from
.envorconfig/settings.cfm, or tamper with the CLI itself. - **Apps with an old
public/Application.cfc.** Until the header-mapping block is copied in, the CLI sends the reload password in the URL query string. It still goes only to the verified own server, and the CLI prints a notice. The password may then appear in that server's own access log. - Unverified read-only output. Read-only commands attached to an unverified server show that server's data. The notice says so.
- Browser-driven URLs are unchanged. Browser-driven
?reload=true&password=...and environment-switch URLs (?reload=<environment>&password=...) keep working as before, and keep the password in the URL. The environment switch relies on the query string surviving the restart redirect. - (Informational) macOS and other users' listeners. An unprivileged
lsofcannot see listeners owned by other OS users. This no longer creates an exposure, because the per-connection check only inspects the verified server's own sockets. A connection accepted by another user's program is never sent anything.
Text from GitHub Advisory Database
Affected versions 1
- wheels-cli, other<= 4.1.0GitHub