Resources
How to set a Content Security Policy (CSP) for your Next.js application
Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)
How to setup nonce with NextJS
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:
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
}
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' },
],
},
],
}
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 };
}
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>
);
}
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.
As shown in the image:
Root cause
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
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>
As shown in the image:
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' />
...
Top comments (0)