fix(serve): refuse to hand the session token to the network - #921
Merged
Conversation
`GET /` is unauthenticated by construction — it is the page that *hands out* `window.__CLAWCODEX_SESSION_TOKEN__`, so the desktop shell and the browser client can both adopt a running backend. That is safe exactly as long as the port is reachable from this machine alone. `clawcodex web` has always enforced it: a non-loopback `--host` is refused unless `--allow-remote` says the caller has put their own authentication in front of it. `clawcodex serve` — the command that actually opens the socket, and the one the desktop spawns — took an arbitrary `--host` straight to uvicorn with no check. So `clawcodex serve --host 0.0.0.0` published a page handing that token to anyone who could reach the port, and the token opens the whole gateway: sessions, prompts, tools. Same app, same route, one gate short. It now carries the same guard and the same flag. Three details that are not cosmetic: - `is_loopback` moves down to `serve_cli`, which is the layer that owns the bind, and `web_cli` imports it rather than keeping a second copy — one predicate, so the two commands cannot drift. - `web_cli` forwards `--allow-remote` to the child. Without that, a `web --host 0.0.0.0 --allow-remote` would clear the parent's gate and then be refused by the child, breaking the documented remote path. - the refusal test stubs `build_app` to raise. Removing the guard otherwise makes it reach uvicorn and block, so a regression would surface in CI as a timed-out job rather than a red test — which is how the first draft of it behaved. The three trust-model comments that asserted the loopback bind as a fact now state it as the condition it is: `desktop_serve`'s module docstring (which owns `_token_ok`), `web_assets`'s note on the token page, and `serve_cli`'s permission-resolution rationale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WAB22BuCSRhjbpR3a8kc5v
Test Results 5 files 1 008 suites 29m 56s ⏱️ For more details on these failures, see this check. Results for commit 24536aa. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while correcting the trust-model comments after #920. It is pre-existing, not from the sidebar work.
The gap
GET /is unauthenticated by construction — it is the page that hands outwindow.__CLAWCODEX_SESSION_TOKEN__, so the desktop shell and the browser client can both adopt a running backend (server/web_assets.py). That is safe exactly as long as the port is reachable from this machine alone.clawcodex webhas always enforced it (web_cli.py:242): a non-loopback--hostis refused unless--allow-remotesays the caller has put their own auth in front.clawcodex servehad no check at all —args.hostwent straight touvicorn.Config(host=...).So
clawcodex serve --host 0.0.0.0published a page handing the session token to anyone who could reach the port, and that token opens the whole gateway:session.create,prompt.submit, tools — at Full Access by default, per the resolver comment in that same file. Same app, same route as the guarded command, one gate short.Default binds are unaffected (
127.0.0.1), and the desktop spawnsservewith an explicit--host 127.0.0.1.The fix
Same guard, same flag as its sibling. Three parts that are not cosmetic:
is_loopbackmoves down toserve_cli, the layer that owns the bind;web_cliimports it instead of keeping a second copy, so the two commands cannot drift.web_cliforwards--allow-remote. Without that,web --host 0.0.0.0 --allow-remotewould clear the parent's gate and then be refused by the child — breaking the documented remote path._serve_argvdid not forward it.build_appto raise. Removing the guard otherwise makes the call reach uvicorn and block, so a regression would appear in CI as a timed-out job rather than a red test. That is how my first draft behaved; mutation-testing it is what surfaced that.Also: three comments that asserted the loopback bind as a fact now state it as the condition it is —
desktop_serve's docstring (the module that owns_token_ok),web_assets's note on the token page, andserve_cli's permission rationale.Compatibility
This is a behaviour change to a shipped command. Anyone running
clawcodex serveon a non-loopback host today must add--allow-remote; the refusal names the flag and says why.959 tests across
tests/entrypoints,tests/serverandtests/nanopass.🤖 Generated with Claude Code
https://claude.ai/code/session_01WAB22BuCSRhjbpR3a8kc5v