Skip to content

fix: block cross-origin state-changing requests (CSRF gate) - #276

Open
NovakPAai wants to merge 1 commit into
mainfrom
fix/csrf-origin-guard
Open

fix: block cross-origin state-changing requests (CSRF gate)#276
NovakPAai wants to merge 1 commit into
mainfrom
fix/csrf-origin-guard

Conversation

@NovakPAai

Copy link
Copy Markdown
Collaborator

What & why

State-changing API routes (/api/focus, /api/launch, /api/bulk-delete, DELETE /api/session, /api/projects/clone, /api/convert, …) had no CSRF/Origin check, and the server JSON.parses the body regardless of Content-Type. A malicious page open in the user's browser could issue a no-preflight "simple" cross-origin POST to http://localhost:PORT/... and trigger real side effects — raise/launch terminals, delete sessions, run git clone.

Follow-up to the INFO finding on #275's security review (pre-existing, cross-cutting — split out per repo "one focused PR" rule).

Fix

A single gate, placed right after the existing loopback Host guard, running before any route: for POST/PUT/DELETE, when Origin (or, as a fallback, Referer) is present it must match our Host, else 403. Absent both = a non-browser client (curl, scripts, the desktop shell's same-origin fetch) → allowed; a browser CSRF attack always carries at least one on a mutating cross-site request. GET/static are never gated.

The same-origin logic is extracted to src/origin-guard.js and now shared by the WebSocket-upgrade check in terminal.js, which previously kept its own copy — so the two can no longer drift. Comparison is host+port, case-normalized (RFC 7230 Host is case-insensitive), scheme ignored (codbash serves plain HTTP on loopback/LAN).

Changes

  • src/origin-guard.js (new) — checkSameOrigin(headers){crossOrigin, reason} and isDisallowedCrossOrigin(method, headers).
  • src/server.js — import the gate, drop the inline helper.
  • src/terminal.jsverifyUpgradeAuth uses the shared checkSameOrigin (adds Referer fallback + case-normalization to the WS path too).
  • test/csrf-origin-guard.test.js — 15 unit tests.

Review handled (security + code review)

  • MEDIUM Referer fallback when Origin absent — added.
  • MEDIUM dedup vs terminal.js — extracted shared module.
  • LOW case-normalize Host compare — done.
  • LOW missing tests (Host-absent fail-closed, case-insensitivity, Referer) — added.
  • INFO scheme-ignored / LAN-bind scope — documented in the module header.

Test plan

  • node --test test/*.test.js — 222 total, 0 fail (1 pre-existing skip); CSRF suite 15/15
  • Live smoke: cross-origin POST → 403; cross-site Referer (no Origin) → 403; same-origin mixed-case Host → reaches route (400); no-header curl → reaches route; cross-origin GET → 200
  • Manual: desktop app + normal browser UI still perform all actions (same-origin, unaffected)

🤖 Generated with Claude Code

…T/DELETE)

State-changing API routes (/api/focus, /api/launch, /api/bulk-delete,
DELETE /api/session, /api/projects/clone, /api/convert, …) had no CSRF/Origin
check, and the server JSON-parses the body regardless of Content-Type. A
malicious page in the user's browser could therefore issue a no-preflight
"simple" cross-origin POST to http://localhost:PORT/... and trigger side
effects (raise/launch terminals, delete sessions, clone repos).

Add a single gate, right after the existing loopback Host guard, that runs
before any route: for POST/PUT/DELETE, when Origin (or, as a fallback, Referer)
is present it must match our Host, else 403. Absent both headers = a non-browser
client (curl, scripts, the desktop shell's same-origin fetch) → allowed; a
browser CSRF attack always carries at least one on a mutating cross-site
request. GET/static are never gated.

The same-origin logic is extracted to src/origin-guard.js and shared by the
WebSocket upgrade check in terminal.js, which previously had its own copy — so
the two can no longer drift. Comparison is host+port, case-normalized (RFC 7230
Host is case-insensitive), scheme ignored (codbash serves plain HTTP).

- src/origin-guard.js: checkSameOrigin (tri-state + reason) + isDisallowedCrossOrigin
- src/server.js: import the gate; drop the inline helper
- src/terminal.js: verifyUpgradeAuth uses the shared checkSameOrigin
- test/csrf-origin-guard.test.js: 15 unit tests (Origin/Referer, case, IPv6, fail-closed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant