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>
)
}
}
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>
)
}
}
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!
Top comments (3)
You still have
unsafe-inline
in your final example though?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...
Ah, TIL, thanks!