fix: block cross-origin state-changing requests (CSRF gate) - #276
Open
NovakPAai wants to merge 1 commit into
Open
fix: block cross-origin state-changing requests (CSRF gate)#276NovakPAai wants to merge 1 commit into
NovakPAai wants to merge 1 commit into
Conversation
…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>
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.
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 serverJSON.parses the body regardless ofContent-Type. A malicious page open in the user's browser could issue a no-preflight "simple" cross-origin POST tohttp://localhost:PORT/...and trigger real side effects — raise/launch terminals, delete sessions, rungit 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, whenOrigin(or, as a fallback,Referer) is present it must match ourHost, 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.jsand now shared by the WebSocket-upgrade check interminal.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}andisDisallowedCrossOrigin(method, headers).src/server.js— import the gate, drop the inline helper.src/terminal.js—verifyUpgradeAuthuses the sharedcheckSameOrigin(adds Referer fallback + case-normalization to the WS path too).test/csrf-origin-guard.test.js— 15 unit tests.Review handled (security + code review)
Test plan
node --test test/*.test.js— 222 total, 0 fail (1 pre-existing skip); CSRF suite 15/15🤖 Generated with Claude Code