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
Check · Review · Land · Live done — Landed and live in production.
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
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,
/healthzand/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
EXEMPTrecord 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
crossOriginwrites, 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-policyfromsecurityFloorfails 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
originGatefails 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
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.
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.tsandsrc/http/origin_gate.tsare unchanged.test/security-floor-every-route.test.ts(new) checksContent-Security-Policy,X-Content-Type-OptionsandReferrer-Policyon each kind of answer:/healthzand/readyzEach 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.tsstill walks every GET route; this test covers the response kinds a route walk misses.test/origin-every-mutation.test.tsistest/cross-origin-mutations.test.tsmoved and tightened:EXEMPTrecord 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.crossOriginwrites, 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):
referrer-policyfromsecurityFloorfails 15 non-page samples, e.g."304 of an asset (304) lacks referrer-policy". Pages still pass because they set the header themselves.originGatefails 34 routes, e.g."POST /:owner/:name.git/git-receive-pack → 200, 0 events, no origin check"."POST /api/v1/user/email/verify → 200, 0 events, no origin check".Before pushing,
bun scripts/gate.tsandbun x tsc --noEmitpassed, and the two new test files plustest/security-floor-routes.test.tspass: 12 tests, 0 failures. Thekeep_plantool 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