Example Next.js CSP Setup

Published —
Technologies — Next.js

This is a simple explanation of how to set up a secure content security policy (CSP) with a default create-next-app project. A working example can be found on github.

To begin with, create a new Next.js project with the default template and the default prompt settings:

$ npx create-next-app@latest

Generate nonces with middleware

A CSP can be set using custom headers in next.config.js, but Next.js includes inline scripts and styles that are will be blocked by a secure CSP using:

script-src 'self';
style-src 'self';

We can add unsafe-inline to the script-src and style-src directives to stop the scripts being blocked, but obviously this is not a secure solution.

To securely allow the inline scripts and styles, a hash-based or nonce-based policy can be used. A nonce-based policy can be implemented in Next.js with middleware. The Next.js docs include an explanation and example implementation of this.

We can add nonces to both the script-src and style-src diretives, so we can securely allow the inline scripts and styles that Next.js includes.

script-src 'self' 'nonce-${nonce}';
style-src 'self' 'nonce-${nonce}';

When the middleware is set up, nonces are automatically to the scripts and styles than Next.js injects.

Force dynamic rendering

When using nonces, a fresh nonce must be generated on each new page-view. To do this you need to 'force dynamic rendering' in Next.js by adding the follwing to the root layout.js page.

export const dynamic = "force-dynamic";

Allow 'unsafe-eval' in dev mode

The Next.js development server uses the Javasript eval funtion, so we need to allow 'unsafe-eval'in development mode.

script-src 'self' 'nonce-${nonce}' ${
process.env.NODE_ENV === "production" ? `` : `'unsafe-eval'`
};

Inline style images

Next/Image adds an inline style (style="color:transparent") to the Img tag that the Next/Image component exports. The default template includes two next/image tags and thus inline styles that will be blocked buy the secure CSP. A nonce can not be added to inline styles, so an alternative approach is needed in this case.

There are open issues relating to this problem - #61388, #45184.

Once solution is to override the next/image component and remove the inline style. The following example is adapted from Sneko's solution using this approach:

import { getImageProps } from "next/image"

export default function Image(props) {
const { props: nextProps } = getImageProps({
...props
})

const { style: _omit, ...delegated } = nextProps

return <img {...delegated} />
}

Then you just need to replace the next/image component import in Page.js with the local version:

// import Image from "next/image";
import Image from "./_components/image";

Summary

To get the default Next.js template working with a secure CSP, you need to do the following:

1. Add a nonce-based CSP using middleware

in /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}' ${
process.env.NODE_ENV === "production" ? `` : `'unsafe-eval'`
};
style-src 'self' 'unsafe-inline';
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
}

2. Force dynamic rendering

in /app/layout.js

export const dynamic = "force-dynamic";

3. Remove next/image inline styles

Override the default next/image component and remove the inline style.

in /app/_components/image.js

import { getImageProps } from "next/image"

export default function Image(props) {
const { props: nextProps } = getImageProps({
...props
})

const { style: _omit, ...delegated } = nextProps

return <img {...delegated} />
}

Finally, update the imports in /app/page.js

// import Image from "next/image";
import Image from "./_components/image";

The CSP should now work in development and production modes without error.

$ npm run dev

$ npm run build && npm run start