DEV Community

Jen C.
Jen C.

Posted on

Security - Solving the "Content Security Policy (CSP) Header Not Set" in Next.js

Resources

Zed Attack Proxy (ZAP)

How to set a Content Security Policy (CSP) for your Next.js application

Middleware

nonce

HTMLElement: nonce property

Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)

'strict-dynamic'

How to setup nonce with NextJS

Matcher

Background

Address the "Content Security Policy (CSP) Header Not Set" issue using ZAP.

Install ZAP and run an automated scan. Below is an example of the generated report:

Image description

Adding a nonce with Middleware

Refer to Next.js official document:

middleware.js

import { NextResponse } from 'next/server'

export function middleware(request) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
`
  // Replace newline characters and spaces
  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
}
Enter fullscreen mode Exit fullscreen mode

You can add a matcher if needed in middleware.js. For example:

...

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

Reading the nonce and adding it to script elements

For example, when using the Next.js Pages Router, you can extract the nonce from the request headers in the _document.js file:

...

static async getInitialProps(context) {
    const props = await super.getInitialProps(context);

    const nonce = context.req.headers['x-nonce'];

    return { ...props, nonce };
  }
Enter fullscreen mode Exit fullscreen mode

In the render function, add the nonce attribute to script elements such as <script>, <Script>, and <NextScript>:

...

  render() {
    const { nonce } = this.props;

    return (
      <Html lang='en'>
        <Head>
          <meta charSet='utf-8' />

          <Script
            nonce={nonce}
            type='application/ld+json'

            ...
          />

          ...

        </Head>
        <body>
          <Main />
          <script nonce={nonce} defer src='/static/icons/svgxuse.js' />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    );
  }
Enter fullscreen mode Exit fullscreen mode

NOTE: According to Accessing nonces and nonce hiding

For security reasons, the nonce content attribute is hidden (an empty string will be returned).

Errors and solutions

Refused to load the script '<URL>' because it violates the following Content Security Policy directive

Error:

Refused to load the script '<URL>' because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-Mjk5MDMxNTctMzU3Mi00ZTk0LWI5MzQtYmZjZWM5ZWQ1ZWJh' <URL> 'strict-dynamic'". Note that 'strict-dynamic' is present, so host-based allowlisting is disabled. Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
Enter fullscreen mode Exit fullscreen mode

As shown in the image:

Image description

Root cause

script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
Enter fullscreen mode Exit fullscreen mode

When the browser supports 'strict-dynamic', 'self' is ignored. Therefore, the error "Refused to load the script" can occur even when running on localhost.

Solution

Since it's not possible to determine if a browser supports 'strict-dynamic' in Next.js middleware, a quick and safe solution is to avoid ignoring matching prefetch requests (from next/link) and static assets.

To do this, remove the entire config (as mentioned above) from middleware.js.

Missing nonce attribute in scrips elements

For example

<script src="/_next/static/chunks/webpack.js" defer=""></script>
Enter fullscreen mode Exit fullscreen mode

As shown in the image:

Image description

Root cause

Missing nonce in <Head>

Solution

In _document.js, add nonce attribute to <Head>:

...

return (
      <Html lang='en'>
        <Head nonce={nonce}>
          <meta charSet='utf-8' />

          ...
Enter fullscreen mode Exit fullscreen mode

Top comments (0)