fix(cli): stop cmd.exe mangling the OAuth URL on Windows (auth login silently fails) - #71
Conversation
`auth login` opened the browser through cmd:
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" })
cmd treats `&` as a command separator, and an OAuth URL is full of them.
Measured with the real URL shape:
in ...authorize?client_id=calle&redirect_uri=http://localhost:9999/cb&state=abc&scope=mcp
out ...authorize?client_id=calle
err 'redirect_uri' is not recognized as an internal or external command
'state' is not recognized as an internal or external command
Two consequences. The browser receives a truncated authorize URL with no
redirect_uri, state or scope, so login cannot complete. And the remaining query
fragments are handed to cmd as commands to run. `stdio: "ignore"` means the user
sees neither -- `calle auth login` appears to do nothing.
The command-execution half is not exploitable here, since the URL comes from
your own broker, but a URL should never reach a shell parser in the first place.
Uses rundll32 url.dll,FileProtocolHandler instead, which takes the URL as a
single argument and never parses it. macOS and Linux paths are unchanged.
Also adds an 'error' listener to the child. Without one, a spawn failure emits
an unhandled 'error' event, which is an uncaught exception -- failing to open a
browser should not take the CLI down when the URL is printed anyway.
Tests: two cases pinning the cmd behaviour and the argv-passing that replaces
it, skipped off Windows. Full suite 113 passing.
Ray-56
left a comment
There was a problem hiding this comment.
Thanks for the focused Windows fix. The URL is no longer parsed by cmd.exe, but this is not ready to merge yet.
[P1] Use a trusted absolute path for the Windows system opener. spawn("rundll32", ...) uses an unqualified executable name. Windows executable search checks the parent process current directory before the System32 directory, so running calle auth login from an untrusted checkout containing a planted rundll32.exe can execute attacker-controlled code. Use the fully qualified System32 executable path (derived from the trusted Windows system directory) or another shell-free API that does not search the working directory. Microsoft documents the search order and recommends fully qualified paths here: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
[P2] Make the regression test exercise the production opener selection. The current tests demonstrate that cmd.exe mangles an ampersand and that Node preserves one argv value, but neither imports or invokes the code changed in cli.js. Reverting production code to cmd.exe would leave both tests green. Please extract a small command-selection helper or inject platform and spawnImpl, then assert the exact trusted executable and argv used by the real code path.
[P2] Add the required patch changeset for @call-e/cli. This changes published runtime behavior, so CONTRIBUTING.md requires a changeset. Run pnpm changeset, pnpm run check:versions, and the CLI pack dry-run.
The CLI check, test, and pack dry-run pass locally, but the two security/coverage items above still block merge.
…real path Addresses the review on CALLE-AI#71. [P1] spawn("rundll32", ...) used an unqualified executable name, so the CreateProcess search order would look in the current working directory before System32 and a planted rundll32.exe in an untrusted checkout would run. The opener is now the fully qualified %SystemRoot%\System32\rundll32.exe. [P2] The opener selection moved into an exported browserOpenCommand(url, platform, env), and the tests now call it instead of demonstrating cmd.exe behaviour beside it. Putting cmd back makes 4 of the 7 fail, on every platform. [P2] Added the patch changeset for @call-e/cli.
|
Both fixed, thanks for the catch on the search order. The opener is now named by its fully qualified path, built from the trusted system directory: The selection moved into an exported |
Ray-56
left a comment
There was a problem hiding this comment.
The current head resolves the previous findings: the Windows opener uses a fully qualified System32 path, the regression tests exercise the production command-selection helper, and the required @call-e/cli patch changeset is present. GitHub CI and the local CLI check, test, version validation, and package dry-run all pass. I did not find another merge blocker.
On Windows,
calle auth loginopens the browser through cmd:cmdtreats&as a command separator, and an OAuth URL is full of them.Measured
Two things go wrong at once:
redirect_uri, nostate, noscope— so login cannot complete.And
stdio: "ignore"means the user sees neither.calle auth loginappears to do nothing at all.The command-execution half is not exploitable here — the URL comes from your own broker — but a URL should not reach a shell parser in the first place.
Fix
rundll32 url.dll,FileProtocolHandler <url>takes the URL as a single argument and never parses it. macOS (open) and Linux (xdg-open) paths are unchanged; both already pass the URL as one argv entry.Also adds an
errorlistener to the child. Without one, a spawn failure emits an unhandlederrorevent, which is an uncaught exception — failing to open a browser should not take the CLI down when the URL is printed anyway.Tests
Two cases, skipped off Windows: one pins the cmd truncation so the old approach cannot come back unnoticed, one asserts the URL survives as a single argv entry. Full suite: 113 passing.
Separate from #70, which fixes the CRLF and path-separator problems in the plugin validators. This one is user-facing rather than contributor-facing.
Disclosure: written with AI assistance. Every measurement above was run on my own Windows 11 machine, and I take responsibility for the change.