Why our CSP is still Report-Only
Every response from this site carries a Content-Security-Policy header, and it is the Report-Only variant. That is deliberate. The reason is a conflict that only appears during real browser navigation.
The policy uses a per-request nonce with strict-dynamic, the shape the framework recommends. Middleware generates a fresh nonce for every request and puts it in script-src. A page's first load gets nonce A, and every script in that HTML carries A. That part is sound.
What breaks is client-side navigation. A browser does not update the CSP header of a page that is already loaded. Click an in-site link and the framework sends an RSC request; middleware hands it a new nonce B, and that response carries B, so the browser inserts a script tagged B. The page's CSP header still allows only A, and the insert is blocked.
The violation has a narrow signature (blocked-uri inline) and appears only on in-browser navigation — a single-page curl is always clean. Among the stored reports, 116 fell into this category, across 42 pages and every major browser.
Two tidier explanations did not survive. It is not that static pages lack a nonce: every page is rendered on demand. It is not the framework bug about a missing nonce on a boundary chunk either: there is no loading.tsx, template.tsx or error.tsx here, and 145 of 145 script tags in the sample carried a nonce.
The tempting fix is unsafe-inline in script-src. It would silence the reports and also disable the nonce mechanism — the part of CSP that matters most here. We are not making that trade. Hash-based CSP is the alternative, at the cost of a heavier build step; a hash cannot express the nonce itself. Until one of those is worth doing, the header stays Report-Only, and the reports keep earning their keep: an accidental eval in one tool is how it was found.