Search code examples
next.jscontent-security-policy

CSP with NextJS 'Refused to execute inline script' in production


I've added a CSP to my Nextjs (v14.0.4) project. I can see that the nonce is being added when I run my project in dev mode. However when I build and start the project the nonces are not added to the next scripts. In the console the following error is shown:

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self' 'nonce-ODkxM2Y4NGEtN2Q3MS00N2E4LWIwOTktODc2YzFjMzFhNjg5' ". Either the 'unsafe-inline' keyword, a hash ('sha256-zlqnbDt84zf1iSefLU/ImC54isoprH/MRiVZGskwexk='), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present.

This is my code. I call this function in the NextJs middleware:

function addCspHeaders(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const isDev = process.env.NODE_ENV !== 'production'
  const nextCsp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ''};
    style-src 'self' 'nonce-${nonce}';
    img-src 'self';
    font-src 'self' data:;
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `

  const cspHeader = nextCsp
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, ' ')
    .trim()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)

  requestHeaders.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue,
  )

  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
  response.headers.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue,
  )

  return response
}

How do I get my code working in production? If I add 'unsafe-inline' to script-src it does work in production, but I do not want to add 'unsafe-inline' to my CSP.


Solution

  • It turns out that the nonces documentation actually gives the answer:

    Every time a page is viewed, a fresh nonce should be generated. This means that you must use dynamic rendering to add nonces.

    I had to add the following line to my root layout.tsx:

    export const dynamic = "force-dynamic"
    

    Also make sure the layout.tsx does not have "use client".