{"schema":"threatline.record/2","record":{"id":"GHSA-x3cm-2j3q-jgg4","aliases":["GHSA-x3cm-2j3q-jgg4"],"summary":"Wheels CLI can send credentials and state-changing commands to a local server that isn't the current project's","details":"## Summary\n\nSeveral 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:\n\n- 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);\n- 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);\n- 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.\n\nRead-only commands (`migrate info`, `db status`/`version`, `routes`, `info`) can likewise report another app's state as the current project's.\n\n## Impact\n\nCredential 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.\n\n## Affected\n\nWheels CLI through 4.1.0 (the `wheels` CLI and the `public/Application.cfc` app template).\n\n## Patches\n\nFixed 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`.\n\nWith 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:\n\n- **Registration.** It is the server this project started (`wheels start`, or `wheels engines rustcfml start`), recorded against this project's directory.\n- **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 `public` directory. A stale or reused process ID is rejected.\n- **Listener.** Every process listening on the server's port is that process.\n- **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.\n\nIn addition:\n\n- Requests go to the address the server actually binds (its loopback address when it binds a wildcard, or its specific address), never to whatever `localhost` resolves to.\n- Responses are read strictly: a truncated or malformed response is an error, never a partial result that could read as success.\n- If ownership cannot be established, the command refuses and names the port.\n- `wheels console` no longer probes other ports.\n- The CLI sends the reload password in an `X-Wheels-Reload-Password` request header instead of the URL query string, so it stays out of access and proxy logs. The updated `public/Application.cfc` accepts 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.\n- 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.\n- `.env` parsing no longer mistakes keys such as `DB_PORT` or `MY_RELOAD_PASSWORD` for the app's port or reload password.\n\n## Workarounds\n\nUntil you can upgrade:\n\n- Give each project a unique port in `lucee.json`, and don't run other servers on it.\n- 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).\n- Avoid `wheels console` and `wheels reload` on machines where other users run servers.\n\n## Limitations\n\n- **Operating-system introspection is required.** Verification reads socket ownership from `/proc` (Linux), `lsof` (macOS) or `netstat` (Windows). Where that isn't available or allowed, the CLI refuses rather than sending, so the command fails but nothing leaks.\n- **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 `.env` or `config/settings.cfm`, or tamper with the CLI itself.\n- **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.\n- **Unverified read-only output.** Read-only commands attached to an unverified server show that server's data. The notice says so.\n- **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.\n- **(Informational) macOS and other users' listeners.** An unprivileged `lsof` cannot 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.\n","categories":["Unclassified"],"product":"wheels-cli","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/wheels-dev/wheels/security/advisories/GHSA-x3cm-2j3q-jgg4","source_modified_at":"2026-10-11T20:04:41Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"other","name":"wheels-cli"},"range":"<= 4.1.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/wheels-dev/wheels/security/advisories/GHSA-x3cm-2j3q-jgg4","source_modified_at":"2026-10-11T20:04:41Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"other","name":"wheels-cli"},"version":"4.1.1"}],"cvss":5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-28T17:09:34Z","modified_at":"2026-10-11T20:04:41Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/wheels-dev/wheels/security/advisories/GHSA-x3cm-2j3q-jgg4","fetched_at":"2026-10-24T20:40:58.529015Z","source_modified_at":"2026-10-11T20:04:41Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-11T20:04:41Z","content_hash":"50e3e09a1fa08fd2e75c03beb80042d01304b63c616c953ce4bab630f88d9cce"}}
