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
|
// Build the Content-Security-Policy. `script-src` uses 'unsafe-inline' because
|
||||||
// per-request nonce instead of 'unsafe-inline'. `style-src` keeps
|
// Next.js 16's Turbopack build does NOT stamp a per-request nonce onto its
|
||||||
// 'unsafe-inline' because Radix / Tailwind / framer-motion inject inline
|
// inline hydration scripts (`self.__next_f.push(...)`). A nonce-based policy
|
||||||
// styles and removing it would break the UI. Next.js reads the nonce from the
|
// therefore blocks those inline scripts and the app never hydrates (blank page).
|
||||||
// `content-security-policy` request header and applies it to its own inline
|
// `style-src` also keeps 'unsafe-inline' (Radix / Tailwind / framer-motion inject
|
||||||
// scripts automatically.
|
// inline styles). NOTE: to restore the stricter nonce-based script policy, build
|
||||||
function buildCsp(nonce: string): string {
|
// with webpack (`next build --webpack`) so Next applies the nonce to its scripts.
|
||||||
|
function buildCsp(): string {
|
||||||
const isDev = process.env.NODE_ENV !== "production"
|
const isDev = process.env.NODE_ENV !== "production"
|
||||||
const sentry = sentryIngestOrigin()
|
const sentry = sentryIngestOrigin()
|
||||||
|
|
||||||
// In development, Next.js/React and Turbopack HMR require eval() for hot
|
// Dev additionally needs 'unsafe-eval' (Turbopack HMR) plus a dev websocket
|
||||||
// reloading and debugging features, and open a dev websocket. These are NOT
|
// (added to connect-src below).
|
||||||
// added in production, where the nonce-based policy stays strict.
|
|
||||||
const scriptSrc = isDev
|
const scriptSrc = isDev
|
||||||
? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval' https://challenges.cloudflare.com`
|
? `script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com`
|
||||||
: `script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com`
|
: `script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com`
|
||||||
const connectSrc = [
|
const connectSrc = [
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
isDev ? "ws: wss:" : "",
|
isDev ? "ws: wss:" : "",
|
||||||
@@ -135,19 +135,10 @@ export async function proxy(request: NextRequest) {
|
|||||||
dropStaleSessionCookie = state === "invalid"
|
dropStaleSessionCookie = state === "invalid"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-request CSP nonce. UUID contains only hex + dashes, so it never
|
const csp = buildCsp()
|
||||||
// includes HTML-escape characters (which Next rejects in nonces).
|
|
||||||
const nonce = crypto.randomUUID()
|
|
||||||
const csp = buildCsp(nonce)
|
|
||||||
|
|
||||||
// Forward the nonce + CSP on the request headers so Next.js can pick up the
|
const response = NextResponse.next()
|
||||||
// nonce and apply it to its own inline scripts during render.
|
// Set the CSP on the outgoing response so the browser enforces it.
|
||||||
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.
|
|
||||||
response.headers.set("Content-Security-Policy", csp)
|
response.headers.set("Content-Security-Policy", csp)
|
||||||
|
|
||||||
if (dropStaleSessionCookie) {
|
if (dropStaleSessionCookie) {
|
||||||
|
|||||||
Reference in New Issue
Block a user