DEV Community

Aaron
Aaron

Posted on

Create a strict Content-Security-Policy in NextJS

First, you need to add this in your middleware:


export function middleware(request: NextRequest) {
    const nonce = Buffer.from(crypto.randomUUID()).toString("base64");

    const csp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    `;
     const cspValue = csp.replace(/\s{2,}/g, " ").trim();

    const reqHeaders = new Headers(request.headers);

    reqHeaders.set("X-Nonce", nonce);
    reqHeaders.set("Content-Security-Policy", cspValue);

    const response= NextResponse.next({
        request: {
             headers: reqHeaders
        }
    });

    response.headers.set("Content-Security-Policy", cspValue);

    return response;
}


// We also don't include csp header when retrieving static assets.

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

All we're doing above is we are generating a nonce which is a cryptographic random string of base64, and attaching that to our CSP for scripts and styles. This CSP will not allow our website to execute inline scripts/styles, except for those injected by scripts that are allowed or those that are whitelisted, and only execute scripts from the domain where the request originated (self, our server). These whitelisted scripts have a nonce attribute to them and contains the generated nonce from the middleware. In NextJS, this nonce will be applied to its automatically-generated scripts/styles (Those that you don't control).

There is one problem, though. This is not applied to other scripts, for example, from Webpack. We need to make this available to Webpack globally. To do that, we need to add this to our root layout file:

export default function RootLayout({
children}: {children: React.ReactNode}) {
    const nonce = request.headers().get("X-Nonce");

    return (
        <html>
             <body>
                  <Script
                      strategy="afterInteractive"
                      id="nonce-script"
                      nonce={nonce}
                      dangerouslySetInnerHTML={{
    __html: `__webpack_nonce__ = ${JSON.stringify(nonce)}`
}}
                  />
                  {children}
             </body>
        </html>
    );
}
Enter fullscreen mode Exit fullscreen mode

This is safe because the point of CSP is to block the loading/execution of assets, so if a script were to exploit this and grab its value, then that means there is something wrong with the way the CSP was made.

We are saying to create a script and activate it once the page has become interactive (hydrated). Now this will become available to all hidden/built-in Webpack packages like singleton.js, etc.

That's all! Thank you for reading, and I hope you learned something. Happy coding! God bless.

Top comments (0)