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

forked from trevato/cs

The security floor stops trusting inline styles: one hashed style block, and 'unsafe-inline' leaves the CSP #202

Merged opened this

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

Merged

`style-src 'self' 'unsafe-inline'` is gone from every response this server sends; the one inline `<style>` block is named by its own SHA-256 instead.

shipped production is serving it deploy ae07c2d

opened this

style-src 'self' 'unsafe-inline' is gone from every response this server sends; the one inline <style> block is named by its own SHA-256 instead.

The shell now owns the exact text it puts between the tags — styleFor(extraStyle) in src/pages/shell.ts, the shared block plus that page's own CSS, escaped the way the document escapes it — and the new src/policy.ts hashes that string into style-src 'self' 'sha256-…'. src/server.ts computes it once per page at start-up: RENDER now holds each game's extraStyle beside the function that draws it (the shooter's, and none for the range and the example, whose block is the shell's exactly), and the 404 and 500 share one hash through src/pages/miss.ts. Responses with no document in them — a bundle, the leaderboard fragment, a 403/413/422 — carry style-src 'self' and accept no inline style at all. Nothing else in the floor moved, no inline style= attribute exists or was added, and 'unsafe-hashes' is not used.

Two tests are the point: one hashes each rendered page's own style block and holds that page's header to it, for every game in the registry plus the 404 and the 500 (deleting extraStyle from RENDER.app fails it); the other scans every route — the three pages, GET and HEAD, the miss, a static bundle, a stored result, 422, 403, 400, 413 and the 500 — and asserts unsafe-inline is in no header and no body. src/policy.test.ts checks the hash against Node's hasher and the empty-string value by hand, and I verified the served pages with openssl dgst -sha256 | openssl base64 against the live header for /, /range, /example and /nope. Three floor-equality tests in src/server.test.ts now compare policies with the hash normalised out, because the hash is deliberately per-page; that is a test change, not a behaviour change, and the range test additionally asserts its policy is not the shooter's.

Two things main was already red on are fixed here, because the check runs them: static/ carried three app.*.js bundles (all stale — the server refuses to boot on that, so the preview was dead), so scripts/build.ts was re-run and the sweep committed; and one line of src/client/main.test.ts did not typecheck. Both are called out rather than folded in silently.

In the preview, the pages should look identical: check /, /range, /example and a wrong path with the console open — a CSP violation would show as unstyled text, and the network tab should still show one HTML request and one bundle. bun run test (typecheck + 887 tests) passes.

🤖 Generated with Claude Code

Closes #129

commented

style-src 'self' 'unsafe-inline' is gone from every response this server sends; the one inline <style> block is named by its own SHA-256 instead.

The shell owns the exact text it puts between the tags — styleFor(extraStyle) in src/pages/shell.ts, the shared block plus that page's own CSS, escaped the way the document escapes it — and the new src/policy.ts hashes that string into style-src 'self' 'sha256-…'. src/server.ts computes it once per page at start-up: RENDER now holds each game's extraStyle beside the function that draws it (the shooter's, and none for the range and the example, whose block is the shell's exactly), and the 404 and 500 share one hash through src/pages/miss.ts. Responses with no document in them — a bundle, the leaderboard fragment, a 403/413/422 — carry style-src 'self' and accept no inline style at all. Nothing else in the floor moved, no inline style= attribute exists or was added, and 'unsafe-hashes' is not used.

Two tests are the point: one hashes each rendered page's own style block and holds that page's header to it, for every game in the registry plus the 404 and the 500 (deleting extraStyle from RENDER.app fails it); the other scans every route — the three pages, GET and HEAD, the miss, a static bundle, a stored result, 422, 403, 400, 413 and the 500 — and asserts unsafe-inline is in no header and no body. src/policy.test.ts checks the hash against Node's hasher and the empty-string value by hand, and I verified the served pages with openssl dgst -sha256 | openssl base64 against the live header for /, /range, /example and /nope. Three floor-equality tests in src/server.test.ts now compare policies with the hash normalised out, because the hash is deliberately per-page; that is a test change, not a behaviour change, and the range test additionally asserts its policy is not the shooter's.

main is merged in. The one conflict was in src/client/main.test.ts, where both sides had fixed the same typecheck error on the mapname assertion — main's mapName constant is the better of the two, so that is what the merge keeps and my ?? null is gone. Main also swept the three stale app.*.js bundles and landed the identical rebuilt bundle, so that part of my first commit is now a no-op; bun run scripts/build.ts after the merge writes nothing new. bun run test (typecheck + 895 tests) passes.

In the preview, the pages should look identical: check /, /range, /example and a wrong path with the console open — a CSP violation would show as unstyled text, and the network tab should still show one HTML request and one bundle.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs