next-secure-headers
next-secure-headers copied to clipboard
Nonce & Hash support for CSP Level 2
🌱 Feature Request
Is your feature request related to a problem? Please describe.
I am attempting to implement a CSP into my app and not use unsafe-inline
.
The way I understand CSP is that for every HTTP request I need to generate a base64 nonce / hash which gets put on the script tag and in the CSP header prefixed with nonce-
.
Describe the solution you'd like
I am not entirely sure on the solution I need here. In my opinion it would be nice to have a solution which allows me to access a generated base64 string and pass it into both the headers and the script tags
Describe alternatives you've considered
I haven't considered any way of approaching this yet but I'll continue to try.
- [x] I've tried to find similar issues and pull requests
- [ ] I would like to work on this feature 💪🏻
Next.js components ( NextScript
and Head
in _document.jsx
) support to accept nonce
Prop in order to implement them like the following.
// _document.jsx
import { randomBytes } from "crypto";
import Document, { Html, Head, Main, NextScript } from "next/document";
export default class extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
const nonce = randomBytes(128).toString("base64");
return { ...initialProps, nonce };
}
render() {
const { nonce } = this.props;
const csp = `script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: 'nonce-${nonce}' 'strict-dynamic'`;
return (
<Html>
<Head nonce={nonce}>
<meta httpEquiv="Content-Security-Policy" content={csp} />
</Head>
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
}
But to generate a hash for every request, Server-Side Rendering is required. In Static Site Generation, a hash is generated on build time, and it doesn't have sufficient effect for prevention. Also, in Incremental Static Regeneration, a hash will be generated on rebuild time, and it doesn't as well.
I've been considering to support nonce from the beginning, but it's currently on hold because I don't want to implement half-baked security features in next-secure-headers. If there is more need, I'd like to implement it. 🙂
EDIT: my example below doesn't seem to work because you cannot dynamically set nonce in headers in next.config :(
Until there is an gSSP in _app.js I will go with this CSP rule per page basis.
Nonce is then accessible in _document.js via res.locals.nonce (careful, it will not be present in pages that are not wrapped or have getStaticProps/static pages)
import { createContentSecurityPolicyHeader } from 'next-secure-headers/lib/rules/content-security-policy';
const withCSPNonce = curry((gSSP, context) => {
if (gSSP && context) {
return Promise.resolve(gSSP(context)).then((pipedProps) => {
if (pipedProps.props) {
const nonce = v4();
const CSP = createContentSecurityPolicyHeader({
directives: {
baseUri: ["'self'"],
defaultSrc: ["'self'", ...srcWhitelist],
styleSrc: ["'self'", "'unsafe-inline'", ...srcWhitelist],
imgSrc: ["'self'", 'data:', 'blob:', 'https:', ...srcWhitelist],
objectSrc: ["'none'"],
scriptSrc: [
`'nonce-${nonce}'`,
"'strict-dynamic'",
"'unsafe-inline'",
`${(isDev && "'unsafe-eval'") || 'https:'}`,
...srcWhitelist,
],
},
});
context.res.setHeader('content-security-policy', CSP.value);
context.res.locals = { nonce };
return {
props: {
...pipedProps.props,
nonce,
},
};
}
return pipedProps;
});
}
throw Error('Either context or gSSP is not provided');
});
Thanks for the package. I was breaking my head around CSP in Next.js. I wanted to get rid of my custom express server where I used Helmet.
I have slightly extended your example since I also need the nonce via script.setAttributte('nonce', nonce)
for inline scripts otherwise safari throws an issue and so on.
I also like to apply the header to the response and not the meta tag:
next.config
const { v4 } = require('uuid')
//.... next.config options
async headers() {
return [
{
source: '/:path*', // attention here, the docs are incorrect (.*) <- not possible
headers: createSecureHeaders({
//...options
contentSecurityPolicy: {
directives: {
//...other rules
scriptSrc: [
`'nonce-${v4()}'`, // set nonce with the CSP headers
...
_document.js
export default class extends Document {
static async getInitialProps(ctx) {
// get the nonce with a regex from headers in NextJs after applying it in next.config
const [, nonce = ''] = /(?:nonce-)([a-z0-9-]+)/gi.exec(
ctx?.res?.getHeader('content-security-policy')
);
return {
...,
nonce
}
... // apply it in <Head nonce={nonce}> etc from const { nonce } = this.props;
Per page if needed
const withCSPNonce = gSSP => context => {
if (gSSP && context) {
return Promise.resolve(gSSP(context)).then((pipedProps) => {
if (pipedProps.props) {
const [, nonce = ''] = /(?:nonce-)([a-z0-9-]+)/gi.exec(
context?.res?.getHeader('content-security-policy')
) || []; // same way to get the nonce as in _document.js
return {
props: {
...pipedProps.props,
nonce, // apply it to pipedProps from getServerSideProps
},
};
}
return pipedProps;
});
}
throw Error('Either context or gSSP is not provided');
});
export default withCSPNonce;
In a page you can wrap with the higher order function
Your pageProps in _app.js or your page will have pageProps.nonce
from the response headers
export const getServerSideProps = withCSPNonce(async(context) => {
//... logic
return {
props: {
.....
}
}
})
Next.js components (
NextScript
andHead
in_document.jsx
) support to acceptnonce
Prop in order to implement them like the following.// _document.jsx import { randomBytes } from "crypto"; import Document, { Html, Head, Main, NextScript } from "next/document"; export default class extends Document { static async getInitialProps(ctx) { const initialProps = await Document.getInitialProps(ctx); const nonce = randomBytes(128).toString("base64"); return { ...initialProps, nonce }; } render() { const { nonce } = this.props; const csp = `script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: 'nonce-${nonce}' 'strict-dynamic'`; return ( <Html> <Head nonce={nonce}> <meta httpEquiv="Content-Security-Policy" content={csp} /> </Head> <body> <Main /> <NextScript nonce={nonce} /> </body> </Html> ); } }
But to generate a hash for every request, Server-Side Rendering is required. In Static Site Generation, a hash is generated on build time, and it doesn't have sufficient effect for prevention. Also, in Incremental Static Regeneration, a hash will be generated on rebuild time, and it doesn't as well.
I've been considering to support nonce from the beginning, but it's currently on hold because I don't want to implement half-baked security features in next-secure-headers. If there is more need, I'd like to implement it. 🙂
This sounds like a good candidate for generation by something like Netlify edge lambdas. Essentially create a known placeholder string in your scripts and replace it dynamically with a generated nonce.