( Photo by FLY:D on Unsplash )
Utilizing CSP (Content Security Policy) allows us to limit the execution of code on our website, thereby mitigating the scope of any potential damage caused by XSS (Cross-Site Scripting) attacks.
In this article, I'll share an example of how to handle CSP headers in Next.js.
tl;dr;
- Set the policy according to Google's advocated Strict CSP
- Use _document.tsx to embed the CSP header into the
head
as<meta httpEquiv="Content-Security-Policy" content={ ... Policy here ... } />
Prerequisites
- Next.js 12.x
- Both SSR (Server Side Rendering) and CSR (Client Side Rendering) are used as rendering methods
- Loading external scripts using the Script component
What is Strict CSP?
Google's proposed Strict CSP is a setting for CSP aimed at realizing higher security.
Strict CSP depends on the application use-case, hence, careful consideration is required to decide how to use it depending on your application's specific use-cases.
What is the strict-dynamic directive?
Strict CSP uses the strict-dynamic
directive to give trust to scripts. strict-dynamic
decides whether to trust a script based on a nonce value or hash value generated from the script. Then, trust is automatically propagated to scripts that are dynamically loaded from that trusted script.
By using the strict-dynamic
directive, the tedious task of managing whitelist becomes unnecessary, as you only need to grant trust to the top-level script.
Implementing Strict CSP in Next.js
The implementation strategy was decided as follows:
- Primarily adhere to Strict CSP
- Trust scripts via nonce
- This is due to the complexity of calculating the hash values of multiple external scripts
- Set the CSP header in the form of a meta tag
- Although it is possible to return response headers via next.config.js, the values cannot be dynamically changed
- I adopted the method of generating a meta tag in _document.tsx to dynamically generate nonce values
Implementation Example
Below is an example of the implementation.
import { randomBytes } from 'crypto'
import { Head, Html, Main, NextScript } from 'next/document'
const Document = () => {
const nonce = randomBytes(128).toString('base64')
const csp = `object-src 'none'; base-uri 'none'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-${nonce}' 'strict-dynamic'`
return (
<Html lang="ja">
<Head nonce={nonce}>
<meta httpEquiv="Content-Security-Policy" content={csp} />
</Head>
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
)
}
export default Document
- By using _document.tsx, you can embed the CSP header in the form of
<meta httpEquiv="Content-Security-Policy" content={ ... } />
.- Although you can set the response header in next.config.js, since the nonce needs to be generated for each page request, the CSP header is returned as a meta tag.
- Specify the following directives in accordance with Strict CSP
object-src 'none'
base-uri 'none'
-
script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-${nonce}' 'strict-dynamic'
- The value of
${nonce}
is a randomly generated dynamic value
- The value of
- Pass the generated nonce value to the
nonce
property of theHead
,NextScript
components imported fromnext/document
.
This is a brief introduction to handling CSP headers in Next.js.
Let's check it out
Looking at the HTML generated by the running application, you'll find that the same nonce value set in the CSP header is set in each script tag.
By the way, if you forget to set the nonce in the Head
component, you can confirm that the execution of the script stops and the following error message is displayed:
Refused to load the script '<URL>' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-JyWbm9+... (snip) ...wURE=' '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.
In conclusion
That's all about how to handle CSP headers in Next.js.
When I was researching how to handle CSP headers in Next.js, I noticed there wasn't much compiled information on the topic, so I decided to compile it myself.
I hope this article is useful to someone.
Top comments (4)
It helped a lot. Thanks and keep up the good work!!
Thanks for your comment!
My experience with Next.js is limited, so if you notice anything, I would appreciate your advice.
using
unsafe-inline
defeats the purpose of having a CSP in first place.Thank you for your comment. I would like to detail why
unsafe-inline
is included in the Content Security Policy (CSP).Modern browser behaviour
Modern browsers support the use of
nonce-
to control inline scripts. Usingnonce-
in combination withunsafe-inline
in modern browsers actually disablesunsafe-inline
.This means that modern browsers with enhanced security features are not exposed to the risks associated with
unsafe-inline
.Compatibility with legacy browsers
Unfortunately, some older browsers do not understand
nonce-
. It is difficult to guarantee full security with these browsers, however, we want to preserve the basic functionality of the site.For this reason, this article uses
unsafe-inline
to allow inline scripts to run.In summary, using
unsafe-inline
for users of older browsers is a trade-off between accepting a reduction in security and prioritising the ability for scripts to function without major changes to the site's structure.