Setting up Content Security Policy in Next.js: Nonces Via Proxies, Best Practices
Content Security Policy is a must-have security feature that lets you control what kind of resources can be loaded on your website, and thus mitigate various types of attacks like Cross-Site Scripting (XSS).
In this article, we will show how to set up CSP for Next.js applications.
The CSP mechanism works via headers. The server includes a header called Content-Security-Policy that contains a list of directives. Each directive specifies rules for a specific type of resource. For example, the script-src directive specifies rules for JavaScript resources, style-src for CSS stylesheets, and so on.
If you want to know what each directive does and how to combine them into a strict policy, refer to our guide on the safest Content Security Policy.
If we don't need to use inline styles or scripts, we can simply set the header in next.config.js:
const cspHeader = [
// in the next section
// we have comments explaining
// what each directive does
`default-src 'self'`,
`script-src 'self'`,
`style-src 'self'`,
`img-src 'self' blob: data:`,
`font-src 'self'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`,
].join("; ");
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: cspHeader.replace(/\n/g, ""),
},
],
},
];
},
};
This is the simplest setup possible and it doesn't prevent the CDN caching the website.
The catch is that it only works if your app has no inline <script> or <style> tags. The moment you (or one of your dependencies) inject one, the browser will block it. Which is a good thing, because inline scripts and styles are a commonly used for XSS attacks.
If you do need inline scripts or styles, you need a nonce.
Using a nonce
A nonce is a mechanism that lets you include a unique token so the browser can verify that inline scripts (or styles) are safe to execute.
The browser simply checks if the <script> (or <style>) tag has a nonce attribute, and if its value matches the one specified in the Content-Security-Policy header. If it does, the browser allows the tag to execute; otherwise, it blocks it.
The official Next.js guide suggests adding a random nonce to the header using a proxy.
I slightly modified it and included comments to explain what each part does:
import { NextRequest, NextResponse } from "next/server";
export function proxy(request: NextRequest) {
// a unique nonce is generated on each request
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
// in dev mode, React relies on 'unsafe-eval' for some of its
// internals (Fast Refresh, error overlays, etc.)
const isDev = process.env.NODE_ENV === "development";
const csp = [
// fallback for any resource type that doesn't have its own directive
`default-src 'self'`,
// controls which scripts can execute, notice nonce-
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""}`,
// controls which stylesheets and inline styles can be applied
`style-src 'self' 'nonce-${nonce}'`,
// where images can be loaded from
`img-src 'self' blob: data:`,
// fonts can only be loaded from the same origin
`font-src 'self'`,
// disables <object>, <embed> and <applet>
// they are a common XSS vector and effectively unused in modern apps
`object-src 'none'`,
// restricts what can appear in a <base> tag, preventing an attacker
// from rewriting the base URL of relative links on the page
`base-uri 'self'`,
// restricts where forms on this page can be submitted to
// = only to same origin
`form-action 'self'`,
// prevents other sites from embedding this page in an iframe,
// so no clickjacking
`frame-ancestors 'none'`,
// upgrades any http:// subresource requests to https://
`upgrade-insecure-requests`,
].join("; ");
// expose the nonce to the app via a request header, so server
// components can read it and stamp it onto inline tags they render
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set("Content-Security-Policy", csp);
return response;
}
After that, all your inline resources should have a nonce attribute with the value from the x-nonce header:
<style nonce="...">
...
</style>
This tells the browser that this particular <style> block is safe to apply, even though it is inline, because it has the correct nonce.
Notice that the nonce is generated on every request. That means your page must be dynamically rendered, which makes it uncacheable for a CDN. It might also increase server load, since the server has to render every request.
I prefer to avoid inline styles and scripts altogether, so I can use a static CSP header that can be cached by a CDN, but if you do need them, this is the way to go.
Where to go next?
Let me also include some resources for further reading:
For how CSP works in general, please refer to the MDN guide.
For the official Next.js documentation on CSP, see this page.
To build a secure CSP policy, check out our free builder.
To test your existing policy, use our free CSP validator.
Consider setting up a reporting endpoint to be notified about any CSP violations on your site.