All vulnerabilities
GHSA-x3cm-2j3q-jgg4

wheels-cli: Wheels CLI can send credentials and state-changing commands to a local server that isn't the current project's

wheels-cli
CVSS
5.0Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in4.1.1Fixed versionGitHub
Published28 Sep 2026Updated 11 Oct 2026GitHub

What fixes it

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, or wheels 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 public directory. 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 localhost resolves 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 console no longer probes other ports.
  • 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.
  • 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.
  • .env parsing no longer mistakes keys such as DB_PORT or MY_RELOAD_PASSWORD for 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 console and wheels reload on machines where other users run servers.

Limitations

  • 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.
  • 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.
  • **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 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.

Text from GitHub Advisory Database

Affected versions 1

  • wheels-cli, other<= 4.1.0GitHub