Public: anyone can read this; the crew can change it.

Every kind of response carries the security floor and every mutation checks its origin, proven by two route-walking tests #10931

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Two route-walking tests now prove the security floor and the origin check. Running them found no real gap: every kind of response already carries the floor, and every mutation is either refused by an origin check or exempt for a stated reason. So `src/http/respond.ts` and `src/http/origin_gate.ts` are unchanged.

shipped production is serving it deploy e08648c

opened this

Two route-walking tests now prove the security floor and the origin check. Running them found no real gap: every kind of response already carries the floor, and every mutation is either refused by an origin check or exempt for a stated reason. So src/http/respond.ts and src/http/origin_gate.ts are unchanged.

test/security-floor-every-route.test.ts (new) checks Content-Security-Policy, X-Content-Type-Options and Referrer-Policy on each kind of answer:

  • pages, signed out and signed in, and a HEAD of a page
  • JSON answers and plain text
  • the stylesheet and sprite assets
  • a 304 for an asset and a 304 for a JSON list, each revalidated with the ETag from the first answer
  • 404s: the page, a repository, the API with no route, the API with no resource, a missing asset and a stale asset hash
  • refusals: 401, 405, 422 for a body that is not JSON, and the origin gate's 403 as JSON and as an HTML page
  • a 308 redirect, OPTIONS, /healthz and /readyz
  • a 500, made by having the database throw on the session read

Each sample also names the status it expects, so a sample that drifts to another kind of answer fails instead of passing without proving anything. security-floor-routes.test.ts still walks every GET route; this test covers the response kinds a route walk misses.

test/origin-every-mutation.test.ts is test/cross-origin-mutations.test.ts moved and tightened:

  • The bearer-only set is now an EXEMPT record with one reason per line. Each entry must be a registered mutation, and a test shows a cookie alone is refused there. This answers the issue's risk of an exemption list that grows without anyone noticing.
  • Each foreign-origin mutation must now also log the "refused cross-origin mutation" line that crossOrigin writes, as well as answering 403 and writing nothing. Before, any 403 passed, so a handler refusing for some other reason could hide a route with no origin check.

The tests fail when a guard is removed (each change was reverted after the run):

  • Removing referrer-policy from securityFloor fails 15 non-page samples, e.g. "304 of an asset (304) lacks referrer-policy". Pages still pass because they set the header themselves.
  • Turning off originGate fails 34 routes, e.g. "POST /:owner/:name.git/git-receive-pack → 200, 0 events, no origin check".
  • Letting the gate skip one path fails on that route alone: "POST /api/v1/user/email/verify → 200, 0 events, no origin check".

Before pushing, bun scripts/gate.ts and bun x tsc --noEmit passed, and the two new test files plus test/security-floor-routes.test.ts pass: 12 tests, 0 failures. The keep_plan tool was not available in this run, so the issue has no plan posted.

To try it: bun test test/security-floor-every-route.test.ts test/origin-every-mutation.test.ts

🤖 Generated with Claude Code

Closes #10918

commented

Two route-walking tests now prove the security floor and the origin check. Running them found no real gap: every kind of response already carries the floor, and every mutation is either refused by an origin check or exempt for a stated reason. So src/http/respond.ts and src/http/origin_gate.ts are unchanged.

test/security-floor-every-route.test.ts (new) checks Content-Security-Policy, X-Content-Type-Options and Referrer-Policy on each kind of answer:

  • pages, signed out and signed in, and a HEAD of a page
  • JSON answers and plain text
  • the stylesheet and sprite assets
  • a 304 for an asset and a 304 for a JSON list, each revalidated with the ETag from the first answer
  • 404s: the page, a repository, the API with no route, the API with no resource, a missing asset and a stale asset hash
  • refusals: 401, 405, 422 for a body that is not JSON, and the origin gate's 403 as JSON and as an HTML page
  • a 308 redirect, OPTIONS, /healthz and /readyz
  • a 500, made by having the database throw on the session read

Each sample also names the status it expects, so a sample that drifts to another kind of answer fails instead of passing without proving anything. security-floor-routes.test.ts still walks every GET route; this test covers the response kinds a route walk misses.

test/origin-every-mutation.test.ts is test/cross-origin-mutations.test.ts moved and tightened:

  • The bearer-only set is now an EXEMPT record with one reason per line. Each entry must be a registered mutation, and a test shows a cookie alone is refused there. This answers the issue's risk of an exemption list that grows without anyone noticing.
  • Each foreign-origin mutation must now also log the "refused cross-origin mutation" line that crossOrigin writes, as well as answering 403 and writing nothing. Before, any 403 passed, so a handler refusing for some other reason could hide a route with no origin check.

The tests fail when a guard is removed (each change was reverted after the run):

  • Removing referrer-policy from securityFloor fails 15 non-page samples, e.g. "304 of an asset (304) lacks referrer-policy". Pages still pass because they set the header themselves.
  • Turning off originGate fails 34 routes, e.g. "POST /:owner/:name.git/git-receive-pack → 200, 0 events, no origin check".
  • Letting the gate skip one path fails on that route alone: "POST /api/v1/user/email/verify → 200, 0 events, no origin check".

Before pushing, bun scripts/gate.ts and bun x tsc --noEmit passed, and the two new test files plus test/security-floor-routes.test.ts pass: 12 tests, 0 failures. The keep_plan tool was not available in this run, so the issue has no plan posted.

To try it: bun test test/security-floor-every-route.test.ts test/origin-every-mutation.test.ts

🤖 Generated with Claude Code

Approved

Test-only pull, scoped to the issue. The floor test samples every kind of answer (pages, JSON, 304s, 404s, 401/405/422/403, redirect, OPTIONS, probes, 500) and asserts the status of each, so a sample that drifts fails. The origin test now requires the logged origin refusal, so a 403 from some other check cannot hide a route with no origin check, and EXEMPT carries one reason per entry. No stale references to the renamed file. Two things to do next: (1) add the same kind-walk for the terminal door (SSH/TUI mutations) so it is covered like the page; (2) make the log-message string match in the origin test import a shared constant from context.ts so a reworded log line fails loudly instead of flagging every route.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs