fix(security): refuse cross-origin writes on a password-less web console by failsafesecurity · Pull Request #3789 · zhayujie/CowAgent · GitHub
Skip to content

fix(security): refuse cross-origin writes on a password-less web console - #3789

Open
failsafesecurity wants to merge 1 commit into
zhayujie:masterfrom
failsafesecurity:security/console-default-auth-csrf-guard
Open

failsafesecurity wants to merge 1 commit into
zhayujie:masterfrom
failsafesecurity:security/console-default-auth-csrf-guard

Conversation

@failsafesecurity

Copy link
Copy Markdown

Finding

Fail-open authentication: default empty web_password leaves every console endpoint unauthenticated

  • Severity: CRITICAL
  • CVSS: 9.3 — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
  • CWE: CWE-306 (Missing Authentication for Critical Function)
  • OWASP: A07:2025 Authentication Failures
  • Scan ID: cmvfi40ut005wru01byjb100s
  • Finding ID: cmvfv37at00c7ru01kzyt4hao

PoC summary (confirmed by Nexus)

_check_auth() at channel/web/core/_common.py:322 returns True whenever
web_password is empty — the shipped default (config-template.json:36,
config.py:281). Every console route that calls _require_auth() therefore
becomes a no-op:

  • POST /message with Content-Type: text/plain and Origin: https://example.com
    returned HTTP 200 (a CORS-simple request, so no preflight blocks it; the
    OPTIONS preflight is rejected 405, so the browser sends the POST anyway).
  • POST /api/workspace/write with no credentials returned HTTP 200 and
    actually overwrote an on-disk workspace file (content replaced).
  • The server log shows /message entering agent mode and dispatching a turn to
    the 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 the
operator 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's Origin (then Referer) host
    against Host. A request with neither header, or with Origin: null (the
    desktop renderer's file:// origin), is treated as local — that is curl / a
    local 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-launch
    X-Cow-Desktop-Token is absent (the desktop shell keeps importing files by
    path).
  • _check_auth() now returns not _passwordless_write_blocked() when no
    password 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.
  • Companion regression test PR adds unit coverage for the guard.

@zhayujie

Copy link
Copy Markdown
Owner

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants