The scan that started it
I pointed securityheaders.com at this site and got a D. One header present — HSTS — and everything else missing: no CSP, no frame protection, no referrer policy. For a site that takes card payments through its shop, that is not a look.
A day later the same scan reads clean. This is what actually changed, and the two trade-offs worth knowing about.
A CSP with nonces, not allowlists
The classic Content-Security-Policy is an allowlist of script origins, and allowlists rot: every new tool wants another origin, and one permissive entry undoes the whole policy. The stronger pattern is per-request nonces with strict-dynamic:
- Middleware mints a fresh nonce for every request and stamps it on the response CSP
- The page's legitimate scripts carry that nonce; injected markup cannot guess it
strict-dynamiclets trusted scripts load their own chunks, which a modern framework needs
Reflected or stored XSS then fails at the browser: the payload arrives without the nonce and never executes.
Trade-off one: styles stay open
style-src still allows inline styles. Server-rendered React serialises style props into attributes, and animation libraries write styles through the CSSOM at runtime; neither can carry a nonce. Style injection is a far weaker attack primitive than script injection, so this is the pragmatic line — but it is a line, and worth stating rather than hiding.
Trade-off two: the fonts moved home
The fonts came from Google's CDN, which meant a third-party origin in the policy and a render-blocking round trip. Self-hosting them through the framework's font pipeline removed both — the CSP now names no font origin at all, and first paint got faster as a side effect.
The one thing the repo could not fix
The reverse proxy in front of the cluster still advertises its server version. That header comes from infrastructure, not the application, and it went on the infrastructure list. A security posture is a stack of layers — the application layer is now the strong one.

Comments