#189: make the CSRF origin check fail closed #240

Merged
stwaidele merged 2 commits from feat/189-csrf-fail-closed into main 2026-07-30 11:02:59 +02:00
Collaborator

A cookie-carrying mutation without Origin and Referer (or with an unparsable one) is rejected with 403 csrf_origin_mismatch instead of passing unchecked.

  • Exception for non-browser clients is structural: PAT/bearer requests carry no session cookie and never reach the check; a request that does carry the cookie is always checked (incl. cookie+bearer combined).
  • Test harness injects the matching Origin (supertest simulates a browser page; browsers always send Origin on mutations), with an explicit x-test-suppress-origin header for the negative cases - the check itself is not weakened.
  • Playwright fixture contexts (contextForUser) send the header on manual context.request seeding calls; the SPA itself always did.
  • release-qa.sh (ops-qa release gate) pins APP_BASE_URL and sends the matching Origin.
  • Dedicated spec csrf.e2e.db.test.ts: missing headers 403, mismatch 403 (kept), unparsable Origin 403 (was an uncaught 500 path), matching passes, GETs untouched, PAT mutation without headers passes.
  • security.md records the fail-closed rule; plan checkbox ticked.

Tested: api full suite 438 green vs fresh DB (incl. 7 new CSRF tests, zero adjustments to the 36 existing cookie-mutation test files); e2e packs auth/social/permission-matrix/attachments/content green vs fresh local env with zero csrf_origin_mismatch in the api log; typecheck + lint green.

Note for scripted cookie clients (curl session workflows): they must now send Origin: <APP_BASE_URL>.

Refs #189 (close manually after green CI + merge).

Generated with Claude Code (Claude Fable 5)

A cookie-carrying mutation without Origin and Referer (or with an unparsable one) is rejected with 403 csrf_origin_mismatch instead of passing unchecked. - Exception for non-browser clients is structural: PAT/bearer requests carry no session cookie and never reach the check; a request that does carry the cookie is always checked (incl. cookie+bearer combined). - Test harness injects the matching Origin (supertest simulates a browser page; browsers always send Origin on mutations), with an explicit x-test-suppress-origin header for the negative cases - the check itself is not weakened. - Playwright fixture contexts (contextForUser) send the header on manual context.request seeding calls; the SPA itself always did. - release-qa.sh (ops-qa release gate) pins APP_BASE_URL and sends the matching Origin. - Dedicated spec csrf.e2e.db.test.ts: missing headers 403, mismatch 403 (kept), unparsable Origin 403 (was an uncaught 500 path), matching passes, GETs untouched, PAT mutation without headers passes. - security.md records the fail-closed rule; plan checkbox ticked. Tested: api full suite 438 green vs fresh DB (incl. 7 new CSRF tests, zero adjustments to the 36 existing cookie-mutation test files); e2e packs auth/social/permission-matrix/attachments/content green vs fresh local env with zero csrf_origin_mismatch in the api log; typecheck + lint green. Note for scripted cookie clients (curl session workflows): they must now send Origin: <APP_BASE_URL>. Refs #189 (close manually after green CI + merge). Generated with Claude Code (Claude Fable 5)
fable-5 added 1 commit 2026-07-30 09:35:04 +02:00
#189: make the CSRF origin check fail closed
Some checks failed
CI / Lint, typecheck, test (pull_request) Failing after 1m51s
CI / Auth e2e pack (pull_request) Has been skipped
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Has been skipped
d32c8c3730
A cookie-carrying mutation without Origin and Referer (or with an
unparsable one) is now rejected with 403 csrf_origin_mismatch instead
of passing unchecked. The exception for non-browser clients stays
structural: PAT/bearer requests carry no session cookie and never reach
the check, and a request that does carry the cookie is always checked.

The test harness injects the matching Origin (supertest simulates a
browser page of this instance) with an explicit suppression header for
the negative cases; the Playwright fixture contexts send the header on
their manual seeding calls; release-qa.sh pins APP_BASE_URL and sends
the matching Origin. Dedicated spec covers: missing headers 403,
mismatch 403, unparsable 403, match passes, GETs untouched, PAT
mutation without headers passes, cookie+bearer still checked.

Refs #189

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
fable-5 added 1 commit 2026-07-30 09:40:07 +02:00
fix flaky tampered-token test: flip a significant signature character
All checks were successful
CI / Build container images (pull_request) Successful in 3m27s
CI / Auth e2e pack (pull_request) Successful in 7m51s
CI / Lint, typecheck, test (pull_request) Successful in 4m49s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m54s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m51s
CI / Import/export fidelity gate (push) Successful in 54s
214e707102
The tampered-token case flipped the LAST base64url character of the
signature. Its low bits are padding that decoders ignore, so whenever a
signature ends in 'A' (~1/16 of tokens) the flip to 'B' decodes to the
same bytes and the token verifies — jose compares decoded bytes, unlike
the pre-#188 homegrown code that compared encoded strings. Reproduced
deterministically (20/20 A-ending signatures accepted the flip); CI run
477 and one local full-suite failure were this, not load. Flipping the
first character makes the tamper always significant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
stwaidele merged commit 214e707102 into main 2026-07-30 11:02:59 +02:00
Sign in to join this conversation.
No description provided.