next-app-router-csp
v0.1.1
Published
Strict CSP (no unsafe-inline) for statically generated Next.js App Router pages, without forcing dynamic rendering.
Maintainers
Readme
next-app-router-csp
Strict script-src (no unsafe-inline) for statically generated Next.js App
Router pages, without forcing dynamic rendering.
The problem
Next.js App Router streams Server Component data as inline
<script>self.__next_f.push(...)</script> tags. They have no src to allow via
'self', their content varies per page, and Next's experimental SRI only adds
integrity to external files. So the documented choices are both bad:
- Allow
'unsafe-inline'inscript-src(defeats the point of a CSP), or - Use a nonce, which forces every page into dynamic rendering and kills static generation / CDN caching.
There is no documented way to get a strict script-src AND keep static
generation on the App Router. This closes that gap.
How it works
A static page's inline scripts are fixed at build time, so their hashes are too.
A single static CSP header can't carry per-page hashes, but a per-page <meta>
tag can. As a postbuild step this:
- Walks the prerendered HTML in your build output.
- Computes a hash (
sha384by default) for every inline<script>, using a real HTML tokenizer (htmlparser2) so the hashed bytes are exactly what the browser hashes - regexes get script bodies and attributes wrong. - Writes each page a
<meta http-equiv="Content-Security-Policy">withscript-src 'self' '<hash>' ..., tagged so re-runs replace it in place.
If a page has inline scripts but no <head> to attach the meta to, the build
fails rather than shipping a page whose scripts silently fall back to the header.
The meta is additive and script-src only: CSP policies stack (a script must
satisfy every active policy), so it tightens scripts to 'self' plus the page's
own inline hashes while your existing CSP header keeps owning connect-src,
style-src, frame-ancestors, and the rest. Nothing else is loosened, and
dynamic (non-prerendered) routes - which have no meta - fall back to your header
policy unchanged.
Result: static generation, CDN caching, and script-src with no unsafe-inline,
all at once.
Usage
After next build, point it at the prerendered app directory:
# package.json
"build": "next build && node node_modules/next-app-router-csp/inject-csp.mjs .next/standalone/.next/server/app"(Use .next/server/app if you are not on output: 'standalone'.) Or call it:
import { injectCsp } from "next-app-router-csp";
await injectCsp(".next/server/app", { algorithm: "sha384" });What it does not do (yet)
- Dynamic / SSR routes. Their inline scripts are per-request, so build-time
hashing can't reach them. Keep
'unsafe-inline'(or a nonce) on those via your header CSP, or use a response-time hasher. Roadmap. style-src. Next also inlines critical CSS. This tool only handles scripts today; pair it withstyle-src 'unsafe-inline'or hash styles separately.frame-ancestors. Not expressible in a meta tag; set it on your HTTP header.
Status
Validated against Next.js 16 App Router (output: 'standalone'): static pages
render and hydrate under a strict no-unsafe-inline script-src, and unauthorized
inline scripts are blocked. The computed hashes match the browser byte-for-byte.
License
MIT
