Loading…
Loading…
Dropping 'unsafe-eval' from the Content-Security-Policy was the right call. It also killed every dynamic import on localhost, while production was perfectly fine.

I removed 'unsafe-eval' from the Content-Security-Policy header one afternoon. Restarted the dev server, opened the homepage, and the 3D block in the hero was an empty box. The console said EvalError.
The part that took a while to understand: production was completely fine. The stricter policy blocked exactly what it was meant to block where real users are, and blocked the wrong thing where only I am.
Exactly one place in the codebase needed it: a new Function("debugger") trick used to detect an open DevTools panel. That is a toy, not a requirement. Once it became opt-in and defaulted to off, the directive had no callers left.
No callers in production. Development was another matter.
The chunks webpack's dev server emits are wrapped in eval(). That is how it attaches a source map to each module so your breakpoints land in the file you actually wrote. Forbid eval and every dynamic import() fails at runtime.
The trap is in the word "dynamic". The page still builds, the layout is still right, most of the site still works. Only the code-split pieces break, and code-splitting is what you apply to precisely the heaviest components: the 3D block, the address lookup dataset. So you lose the ability to verify the features that are hardest to verify, while the page looks normal.
Nothing in the build output warns you either. Code-splitting is a runtime concern, so the failure surfaces as one console line at the moment a reader would have scrolled to the component, and never during the build that was supposed to catch it.
`script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://accounts.google.com${
isDev ? " 'unsafe-eval' https://va.vercel-scripts.com" : ""
}`
The appended string only exists when isDev is true. The header served in production is byte-for-byte what it was, and localhost gets eval back.
va.vercel-scripts.com rides along for a related reason: in production the analytics package loads its script from the site's own origin, which 'self' already covers. In development it swaps in a debug build hosted elsewhere, which the policy would block with a single console line and no other symptom.
'wasm-unsafe-eval' took its place. The 3D block has to compile the physics library's WASM, and this directive permits that without reopening eval for JavaScript. It landed in Chrome 97, Safari 16.4 and Firefox 102, which covers the project's browserslist target.
'unsafe-inline' has to stay, and that one is genuinely annoying. Next emits inline bootstrap and RSC payload scripts on every page. The correct alternative is a nonce, and a nonce has to differ per request. A prerendered page has no "per request" to generate one in. Removing 'unsafe-inline' means giving up static rendering, and for this site that is a much worse trade.
This is the kind of trade-off CSP guides rarely state plainly. The perfect policy exists on paper. The policy you ship is the strictest one still compatible with your rendering architecture.
Afterwards I added a rule: any change touching WebGL, WASM or dynamic imports gets verified against a production build, not the dev server.
The reasoning is uncomfortably symmetric. The dev server relaxes the policy, so something broken in production can look healthy on localhost. But the reverse holds too, and that is what happened here: development can break on a rule production handles without complaint. Both directions land on the same conclusion. The empty box on your screen tells you nothing about that box on a reader's screen.
No comments yet — be the first!