DEV Community

Cover image for Securing Your Next.js Application with Strict CSP
Shinji NAKAMATSU
Shinji NAKAMATSU Subscriber

Posted on

Securing Your Next.js Application with Strict CSP

( 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.

How trust is propagated by strict-dynamic

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


Enter fullscreen mode Exit fullscreen mode
  • 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
  • Pass the generated nonce value to the nonce property of the Head, NextScript components imported from next/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.

Generated HTML

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.

Enter fullscreen mode Exit fullscreen mode




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.

References

Top comments (4)

Collapse
 
soobinrho profile image
Soobin Rho

It helped a lot. Thanks and keep up the good work!!

Collapse
 
snaka profile image
Shinji NAKAMATSU

Thanks for your comment!

My experience with Next.js is limited, so if you notice anything, I would appreciate your advice.

Collapse
 
drandoll profile image
d-randoll

using unsafe-inline defeats the purpose of having a CSP in first place.

Collapse
 
snaka profile image
Shinji NAKAMATSU • Edited

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. Using nonce- in combination with unsafe-inline in modern browsers actually disables unsafe-inline.

Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set for older browsers without nonce support.
developer.mozilla.org/en-US/docs/W...

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.