DEV Community

Cover image for How to deploy a strict Content Security Policy (CSP) with Next.js
Guy Dumais
Guy Dumais

Posted on • Originally published at guydumais.digital

How to deploy a strict Content Security Policy (CSP) with Next.js

Based on the most comprehensive study to date from Google, 95% of real-world Content Security Policy (CSP) deployments are bypassed and 99.34% of hosts with CSP use policies that offer no benefit against XSS.

This is why Google suggests that the model of designating trust by specifying URL whitelists from which scripts can execute should be replaced with an approach based on nonces and hashes, already defined by the CSP specification and available in major browser implementations. Hence the name strict CSP.

In the context of a Single Page App (SPA) such as the Next.js React framework, we need to use a Hashed-based CSP in order to properly integrate a strict CSP which will offer real protection against CSS attacks.

Depending on the complexity of your application, the integration of a hash-based Content Security Policy could be trivial and require a lot of code manipulation. That’s why I’ve come to build a package on NPM specifically designed for Next.js to allow developers to integrate strict CSP in a snap with just a few lines of code.

next-strict-csp

next-strict-csp is a hash-based Strict Content Security Policy generator for Next.js that is easily integrated in the _document.tsx file of your Next.js application. Once in production, it will automatically inject the hashes into the content security policy meta tag and protect against XSS once deployed and cached on CDN.

Here is an example of _document.tsx with basic integration of next-strict-csp:

...

// Next.js libraries
import Document, { Html, Head, Main, NextScript } from 'next/document'

// Next Strict Content Security Policy
import { NextStrictCSP } from 'next-strict-csp'

...

// Enable Head Strict CSP in production mode only
const HeadCSP = process.env.NODE_ENV === 'production' ? NextStrictCSP : Head

...

// Document component
class MyDoc extends Document {

  render() {
    return (
      <Html>
        <HeadCSP>
          { process.env.NODE_ENV === 'production' && 
          <meta httpEquiv="Content-Security-Policy" />
          }

          ...

        </HeadCSP>
        <body>

          ...

          <Main />
          <NextScript />

          ...

        </body>
      </Html>
    )
  }

}
Enter fullscreen mode Exit fullscreen mode

Once put live in production, you'll get a content security policy meta tag that looks like this:
<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-XOzjewwkvGMLaoj+oYCiOZ3kRwb6RT1Ph6vn4qL+XI0=' 'unsafe-inline' http: https:;" slug="/">

With a strict CSP, you need to hash your inline scripts as well. This could be easily achieved with next-strict-csp by integrating your inline scripts in an array like this in _document.tsx:

...

// Next.js libraries
import Document, { Html, Head, Main, NextScript } from 'next/document'

// Next Strict Content Security Policy
import { NextStrictCSP } from 'next-strict-csp'

...

// Cloudflare Insights Script (Optional)
const cloudflareJs = `var s = document.createElement('script')
s.src = 'https://static.cloudflareinsights.com/beacon.min.js'
s.setAttribute('data-cf-beacon', '{"token": "YOUR CLOUDFLARE WEB ANALYTICS TOKEN STRING"}')
document.body.appendChild(s)`

// Google Tag Manager Script (Optional)
const GTMJs = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','YOUR GOOGLE TAG MANAGER ID STRING');`

// Next Strict CSP
// Inline scripts to hash  (Optional)
NextStrictCSP.inlineJs = [
  cloudflareJs,
  GTMJs
]

...

// Enable Head Strict CSP in production mode only
const HeadCSP = process.env.NODE_ENV === 'production' ? NextStrictCSP : Head

...

// Document component
class MyDoc extends Document {

  render() {
    return (
      <Html>
        <HeadCSP>
          { process.env.NODE_ENV === 'production' && 
          <meta httpEquiv="Content-Security-Policy" />
          }

          ...

          {/* Google Tag Manager */}
          { process.env.NODE_ENV === 'production' && 
          <script 
            dangerouslySetInnerHTML={{
                __html: GTMJs
            }}
          />
          }
          {/* End Google Tag Manager */}

        </HeadCSP>
        <body>
          { process.env.NODE_ENV === 'production' && 
          <noscript
            dangerouslySetInnerHTML={{
                __html: `<iframe src="https://www.googletagmanager.com/ns.html?id=YOUR GOOGLE TAG MANAGER ID STRING" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
            }}
          />
          }

          ...

          <Main />
          <NextScript />
          {/* Cloudflare Web Analytics */}
          {/*<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon={`{"token": "YOUR CLOUDFLARE WEB ANALYTICS TOKEN STRING"}`}></script>*/}
          {process.env.NODE_ENV === 'production' && 
          <script dangerouslySetInnerHTML={{
            __html: cloudflareJs
          }} />
          }
          {/* End Cloudflare Web Analytics */}

          ...

        </body>
      </Html>
    )
  }

}
Enter fullscreen mode Exit fullscreen mode

Once live, your inline scripts hashes will be injected into the content security policy meta tag like this:
<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-XOzjewwkvGMLaoj+oYCiOZ3kRwb6RT1Ph6vn4qL+XI0=' 'sha256-QSxH3dqIUPdeBvyxSZSuIbZfgtCo/yuqnzU+5gtq9Ak=' 'sha256-7V8IQTE3j8PL2rD62J9XsmUhchGnkkyNIQfwoYVK04I=' 'unsafe-inline' http: https:;" slug="/">

Last word

Hope you enjoyed next-strict-csp and don't hesitate to leave a comment to let me know what you think about it.

Thanks for reading, see you!

Discussion (3)

Collapse
inhuofficial profile image
InHuOfficial

You still have unsafe-inline in your final example though?

Collapse
guydumais profile image
Guy Dumais Author

To ensure compatibility with very old browser versions (4+ years), you can add 'unsafe-inline' as a fallback. All recent browsers will ignore 'unsafe-inline' if a CSP nonce or hash is present.

https: and unsafe-inline don't make your policy less safe because they will be ignored by browsers which support strict-dynamic.

Source:
web.dev/strict-csp/#what-is-a-stri...

Collapse
inhuofficial profile image
InHuOfficial

Ah, TIL, thanks!