fix(csp): allow inline scripts so the app hydrates on Turbopack builds
Next.js 16 builds with Turbopack, which does NOT stamp the middleware CSP nonce onto its inline hydration scripts (self.__next_f.push). The nonce-based `script-src 'self' 'nonce-…'` therefore blocked those inline scripts, React never hydrated, and the marketing/app pages rendered as a blank/black shell (header + framer-motion sections stuck at opacity:0). Switch `script-src` to 'self' 'unsafe-inline' (Turbopack-compatible) and drop the now-unused nonce plumbing. All other CSP directives stay strict (object-src 'none', frame-ancestors 'none', locked connect-src/frame-src). Verified in a local production container: served script-src is correct and the page hydrates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5495b94924
commit
0d11018019
@@ -41,22 +41,22 @@ function sentryIngestOrigin(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
// Build the per-request Content-Security-Policy. `script-src` carries a
|
||||
// per-request nonce instead of 'unsafe-inline'. `style-src` keeps
|
||||
// 'unsafe-inline' because Radix / Tailwind / framer-motion inject inline
|
||||
// styles and removing it would break the UI. Next.js reads the nonce from the
|
||||
// `content-security-policy` request header and applies it to its own inline
|
||||
// scripts automatically.
|
||||
function buildCsp(nonce: string): string {
|
||||
// Build the Content-Security-Policy. `script-src` uses 'unsafe-inline' because
|
||||
// Next.js 16's Turbopack build does NOT stamp a per-request nonce onto its
|
||||
// inline hydration scripts (`self.__next_f.push(...)`). A nonce-based policy
|
||||
// therefore blocks those inline scripts and the app never hydrates (blank page).
|
||||
// `style-src` also keeps 'unsafe-inline' (Radix / Tailwind / framer-motion inject
|
||||
// inline styles). NOTE: to restore the stricter nonce-based script policy, build
|
||||
// with webpack (`next build --webpack`) so Next applies the nonce to its scripts.
|
||||
function buildCsp(): string {
|
||||
const isDev = process.env.NODE_ENV !== "production"
|
||||
const sentry = sentryIngestOrigin()
|
||||
|
||||
// In development, Next.js/React and Turbopack HMR require eval() for hot
|
||||
// reloading and debugging features, and open a dev websocket. These are NOT
|
||||
// added in production, where the nonce-based policy stays strict.
|
||||
// Dev additionally needs 'unsafe-eval' (Turbopack HMR) plus a dev websocket
|
||||
// (added to connect-src below).
|
||||
const scriptSrc = isDev
|
||||
? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval' https://challenges.cloudflare.com`
|
||||
: `script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com`
|
||||
? `script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com`
|
||||
: `script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com`
|
||||
const connectSrc = [
|
||||
"connect-src 'self'",
|
||||
isDev ? "ws: wss:" : "",
|
||||
@@ -135,19 +135,10 @@ export async function proxy(request: NextRequest) {
|
||||
dropStaleSessionCookie = state === "invalid"
|
||||
}
|
||||
|
||||
// Per-request CSP nonce. UUID contains only hex + dashes, so it never
|
||||
// includes HTML-escape characters (which Next rejects in nonces).
|
||||
const nonce = crypto.randomUUID()
|
||||
const csp = buildCsp(nonce)
|
||||
const csp = buildCsp()
|
||||
|
||||
// Forward the nonce + CSP on the request headers so Next.js can pick up the
|
||||
// nonce and apply it to its own inline scripts during render.
|
||||
const requestHeaders = new Headers(request.headers)
|
||||
requestHeaders.set("x-nonce", nonce)
|
||||
requestHeaders.set("Content-Security-Policy", csp)
|
||||
|
||||
const response = NextResponse.next({ request: { headers: requestHeaders } })
|
||||
// Also set the CSP on the outgoing response so the browser enforces it.
|
||||
const response = NextResponse.next()
|
||||
// Set the CSP on the outgoing response so the browser enforces it.
|
||||
response.headers.set("Content-Security-Policy", csp)
|
||||
|
||||
if (dropStaleSessionCookie) {
|
||||
|
||||
Reference in New Issue
Block a user