{{ message }}
Repository navigation
fix(security): refuse cross-origin writes on a password-less web console - #3789
Open
failsafesecurity wants to merge 1 commit into
Open
failsafesecurity wants to merge 1 commit into
failsafesecurity wants to merge 1 commit into
Conversation
Owner
This branch has not been deployed
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.

Finding
Fail-open authentication: default empty
web_passwordleaves every console endpoint unauthenticatedCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:Ncmvfi40ut005wru01byjb100scmvfv37at00c7ru01kzyt4haoPoC summary (confirmed by Nexus)
_check_auth()atchannel/web/core/_common.py:322returnsTruewheneverweb_passwordis empty — the shipped default (config-template.json:36,config.py:281). Every console route that calls_require_auth()thereforebecomes a no-op:
POST /messagewithContent-Type: text/plainandOrigin: https://example.comreturned HTTP 200 (a CORS-simple request, so no preflight blocks it; the
OPTIONSpreflight is rejected 405, so the browser sends the POST anyway).POST /api/workspace/writewith no credentials returned HTTP 200 andactually overwrote an on-disk workspace file (content replaced).
/messageentering agent mode and dispatching a turn tothe full-access agent — only the missing upstream LLM key stopped tool-call
execution.
The default binds
127.0.0.1, but loopback is reachable from any page theoperator has open, so an attacker page can drive the full-access agent or write
arbitrary workspace files without any credential.
Fix
The console needs to stay usable out of the box (reads on loopback, the desktop
shell), so this does not force a password. Instead it closes the specific
weakness: with no password configured, a state-changing request that a
browser is making from another origin is now refused.
_origin_is_foreign()compares the request'sOrigin(thenReferer) hostagainst
Host. A request with neither header, or withOrigin: null(thedesktop renderer's
file://origin), is treated as local — that is curl / alocal script / the desktop shell, not a cross-site browser call.
_passwordless_write_blocked()refuses only non-idempotent methods(
POST/PUT/PATCH/DELETE) and only when the per-launchX-Cow-Desktop-Tokenis absent (the desktop shell keeps importing files bypath).
_check_auth()now returnsnot _passwordless_write_blocked()when nopassword is set, so the CSRF guard applies before the fail-open branch.
Reads are unaffected, and setting a password still yields the same 401 for a
missing credential as before.
Verification
python3 -m py_compile channel/web/core/_common.py— clean.