Repository navigation
wiki: security headers on Worker responses + report-only CSP - #47
Conversation
Every Worker-generated response now carries HSTS, X-Content-Type-Options: nosniff, an enforced frame-ancestors 'none' CSP plus X-Frame-Options: DENY, Referrer-Policy, and a Content-Security-Policy-Report-Only policy that matches what the pages load today (inline scripts/styles, the same-origin /brain iframe, Wikipedia media, OAuth avatars, the Cloudflare Web Analytics beacon). Violation reports go to a log-only POST /api/csp-report sink. Headers are applied at response time and never enter RENDER_CACHE bytes, so no render-cache key bump. Static assets (wiki/public, including /brain) are served before the Worker runs and are not covered.
…e report sink - font-src allows the JuliaMono webfont /review loads from jsDelivr; img-src/media-src allow github.com (GitHub-rendered comment attachments). - /api/csp-report counts body bytes while streaming (a chunked body without Content-Length can no longer buffer unbounded), raises the cap to 64 KB so Reporting API batches fit, and logs one line per request. - Correct the frame-ancestors comment: report-only reports, it does not block.
|
Review status: merged in Update 2026-10-14 (re-checked at
Original review (2026-10-14)Independent adversarial review of Verdict: keep the Worker security headers; drop the report-only CSP and the Required before merge
Decisions for the maintainer
Merge order: release-affecting; merge with the other Fixes are in progress; this comment will be updated. Posted by the WikiLean PR swarm (lead); updated 2026-10-14. |
Status review (2026-10-14): 🔧 One-line fix before mergeThe headers are correct and break nothing live. Framing is denied, nosniff and HSTS are set, and the CSP is report-only and covers every origin seen on live pages. In a combined build with #46, #50, #54, #62 and #63, typecheck and the worker suite pass. Fix before merge
Nits
Conflicts: #54 conflicts in To ship: Worker change, so it needs the next Brain release and promotion. HSTS cannot protect a visitor's first plain-HTTP request, so confirm the zone's HTTP→HTTPS redirect (Always Use HTTPS) is on before the release ships this header. The PR's "HTTPS-only" risk note depends on it. 🤖 Automated status pass by Claude Code over all 18 open PRs (#46–#63), posted for the repo owner. An independent verifier re-checked each finding. All 18 heads were also merged and tested together in a scratch tree; the only test failure that comes from combining them is #47 × #55 (three other pairs had mechanical text conflicts). Reviewed head: |
…bout cache test - media-src adds https://*.githubusercontent.com: GitHub-rendered comment HTML on /review gives uploaded videos a private-user-images src (img-src already allowed the host); the test pins it. - The security-headers cache test checks for any page:about:* key instead of page:about:v1, so an /about cache-key bump does not break it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes pushed (2026-10-14)This addresses the two earlier reviews on this PR (the "Status review (2026-10-14)" comment and the PR swarm's review): the What changed
Verification
Not changed / still open
Merge notes
🤖 Posted by Claude Code for the repo owner. Each fix was checked by an independent verifier before it was pushed. Head: |
Resolves the wiki/src/index.ts import conflict with #47 by keeping both imports; registerVersionRoutes stays after registerSecurityHeaders (and registerRepoRoutes), so the security headers wrap /api/version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the wiki/src/index.ts import conflict with #47 by keeping both imports; registerVersionRoutes stays after registerSecurityHeaders (and registerRepoRoutes), so the security headers wrap /api/version.
wiki: security headers on Worker responses + report-only CSP
Problem
The Worker sets no browser security headers. On 2026-10-13,
curl -sD- https://wikilean.jackmccarthy.org/{,about,login,stats}returned noStrict-Transport-Security,X-Content-Type-Options,X-Frame-Options/frame-ancestors,Referrer-PolicyorContent-Security-Policy. The home page's headers were onlydate, content-type, server-timing, report-to, nel, server, cf-ray, alt-svc.git grep -i 'Content-Security-Policy\|X-Frame-Options\|Strict-Transport' wiki/srcat 37b7036 finds nothing.The site renders user-contributed annotations, so a single escaping bug in the renderer has no defense-in-depth behind it.
Change
New
wiki/src/security.ts, registered first inindex.tsso it wraps every route,notFoundandonError. Every Worker-generated response now carries:Strict-Transport-Securitymax-age=31536000; includeSubDomainsX-Content-Type-OptionsnosniffX-Frame-OptionsDENYContent-Security-Policy(enforced)frame-ancestors 'none'Content-Security-Policy-Report-OnlyReporting-Endpointscsp="/api/csp-report"Referrer-Policystrict-origin-when-cross-originThe report-only policy was built from what live pages load today, checked against captures of
/,/about,/stats,/login, an article, history,/recent-changes,/articles,/review,/queue,/wikifunctionsand/mcp:<script>/<style>blocks andstyle=attributes ('unsafe-inline'), plus same-origin assets andfetchcalls;static.cloudflareinsights.comscript,cloudflareinsights.comconnect);<iframe src="/brain?embed=1">(frame-src 'self');*.wikimedia.org,wikimedia.org);*.githubusercontent.com,*.googleusercontent.com);/review: GitHub comment media (github.com, plus*.githubusercontent.cominmedia-src, since uploaded videos render with aprivate-user-images.githubusercontent.comsrc) and the JuliaMono webfont (font-src https://cdn.jsdelivr.net);object-src 'none',base-uri 'self',form-action 'self'.The framing ban is enforced through its own header, because a report-only
frame-ancestorsonly reports.New
POST /api/csp-reportis a log-only sink. It accepts both report shapes (legacyreport-uriand Reporting API batches) and writes one structuredcsp-reportline per request to Workers Logs, holding at most 20 violations with fields truncated. The body is byte-capped at 64 KB while it streams, and nothing is stored. The path is under/api, whichRESERVEDalready lists.Render cache: headers are set at response time and never enter the
RENDER_CACHEbytes, so no cache key changes. A test asserts the cached values contain no header text.Not covered: files in
wiki/public(including/brain,/conceptsand the Brain shards) are served by Cloudflare before the Worker runs, so these headers don't reach them. Covering them needs a_headersasset emitted by build-public, which changes the release asset tree. That would be a follow-up, and/brainwould needframe-ancestors 'self'/SAMEORIGIN, notDENY, because the landing page frames it.How tested
npx vitest run test/security-headers.test.ts: 8 pass. It pins the exact headers on:Response.redirect(immutable headers, re-wrapped).It also checks that the CSP directives cover the iframe, the beacon, the webfont, GitHub comment media and Wikimedia, that both report shapes are logged as one line, and that oversized, chunked and malformed report bodies are rejected.
npx tsc --noEmitpasses.Risks
max-age(1 year) expires. It assumes the zone's HTTP-to-HTTPS redirect ("Always Use HTTPS") is enabled before a release ships this header. The wiki host has no subdomains forincludeSubDomainsto reach.X-Frame-Options: DENYstops any third party from framing Worker pages. Nothing on the site frames a Worker route.csp-reportlog lines for anything the policy misses.Release-affecting: ships only through the Brain release and promotion path.
What Jack must do
Before the release that ships this, make sure the zone's "Always Use HTTPS" redirect is enabled. After it ships, watch Workers Logs for
"event":"csp-report", adjust the policy, then switch it to enforced.