Public: anyone can read this; the crew can change it.
The security floor is on every response, not only the HTML ones #165
Check · Review · Land · Live done — Landed and live in production.
Pushed. Live check against a booted forge confirms the floor on `/`, `/api/v1/repos`, `/assets/forge.css`, `/healthz` and a 404.
shipped production is serving it deploy caaa084
Sign in to join this conversation.
new above
offline
Pushed. Live check against a booted forge confirms the floor on
/,/api/v1/repos,/assets/forge.css,/healthzand a 404.---
Every response the forge writes now leaves with
x-content-type-options: nosniffandreferrer-policy: same-origin, and every non-document one withdefault-src 'none'; frame-ancestors 'none'; base-uri 'none'— so a JSON body carrying repository names, issue titles and comment text is inert even if a browser is talked into treating it as markup. The decision lives in one function,securityFloor(src/http/respond.ts), applied at one door: the fetch boundary insrc/http/server.ts. To give the wrapper a single expression to wrap, the router half offetchmoved out verbatim intoanswer(app, router, limiter, request)— the diff is a move plus indentation, no logic changed, and the#137comment about the viewer-lessCtxmoved with it; nothing in the wrapper resolves auth.A responder that already set one of these headers keeps its value, so
page()keeps its nonce'd policy andfragment()its sandbox one. Which policy a response gets is decided by thecontent-typethat actually left, not byprefersHtmlas suggested on the issue: the question at this seam is "is this body a document?", andAcceptanswers a different one — a browser navigating to a JSON route sendsAccept: text/htmland would have been handed the exemption.prefersHtmlstill decides the 404 shape, one call, unchanged. Headers are set on the response that exists rather than on a copy, the same tradewithRateHeadersmakes one line up, so the SSE log follow is never re-read; an immutable header set is caught and left alone. The wrapper sits afterproxyAppreturns, so a preview's headers are still entirely the app's — that is true by construction, not by care.Git smart-HTTP goes through the router and so now carries the floor; git ignores all three. Checked by running
test/app.test.tsandtest/web.test.ts, which clone, commit and push over HTTPS against a live forge — both green (in this sandbox they needGIT_CONFIG_COUNTunset, since the agent environment injects anhttp.extraHeaderAuthorization that overrides the test's own credentials; they fail identically without my change). Cost measured at ~1.3 µs per response for threehasand threeset— about 0.03% of a 5 ms page.To try it:
curl -sI http://…/api/v1/reposand/assets/forge.cssand/healthz, thencurl -sI /to see the nonce'd page policy survive. The new test intest/web.test.tswalks an HTML page, JSON, error JSON,no_routeJSON, the stylesheet, the built asset, a redirect and/healthz; with the boundary reverted it fails on the first JSON response.bun x tsc --noEmitand thecheckcommand inforge.jsonare green (322 pass, 0 fail). I did not touch the per-responder headers themselves, the internal unix socket, or app-side responses.🤖 Generated with Claude Code
Closes #136