Using CSP in NextJS, nginx and Material-ui(SSR)
Solution 1:
The solution I found was to add nonce value to the inline js and css in _document.tsx
_document.tsx
Generate a nonce using uuid v4 and convert it to base64 using crypto nodejs module. Then create Content Security Policy and add the generated nonce value. Create a function to accomplish to create a nonce and generate CSP and return the CSP string along with the nonce
Add the generated CSP in the HTML Head and add meta tags.
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';
import crypto from 'crypto';
import { v4 } from 'uuid';
// import theme from '@utils/theme';
/**
* Generate Content Security Policy for the app.
* Uses randomly generated nonce (base64)
*
* @returns [csp: string, nonce: string] - CSP string in first array element, nonce in the second array element.
*/
const generateCsp = (): [csp: string, nonce: string] => {
const production = process.env.NODE_ENV === 'production';
// generate random nonce converted to base64. Must be different on every HTTP page load
const hash = crypto.createHash('sha256');
hash.update(v4());
const nonce = hash.digest('base64');
let csp = ``;
csp += `default-src 'none';`;
csp += `base-uri 'self';`;
csp += `style-src https://fonts.googleapis.com 'unsafe-inline';`; // NextJS requires 'unsafe-inline'
csp += `script-src 'nonce-${nonce}' 'self' ${production ? '' : "'unsafe-eval'"};`; // NextJS requires 'self' and 'unsafe-eval' in dev (faster source maps)
csp += `font-src https://fonts.gstatic.com;`;
if (!production) csp += `connect-src 'self';`;
return [csp, nonce];
};
export default class MyDocument extends Document {
render(): JSX.Element {
const [csp, nonce] = generateCsp();
return (
<Html lang='en'>
<Head nonce={nonce}>
{/* PWA primary color */}
{/* <meta name='theme-color' content={theme.palette.primary.main} /> */}
<meta property='csp-nonce' content={nonce} />
<meta httpEquiv='Content-Security-Policy' content={csp} />
<link
rel='stylesheet'
href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'
/>
</Head>
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};
};
source: https://github.com/vercel/next.js/blob/master/examples/with-strict-csp/pages/_document.js
nginx config
make sure to remove adding header regarding Content Security Policy. It might override the CSP in _document.jsx file.
alternative solutions
Creating a custom server and injecting nonce and Content Security Policy that can be accessed in _document.tsx
- https://bitgate.cz/content-security-policy-inline-scripts-and-next-js/
- https://nextjs.org/docs/advanced-features/custom-server
- https://medium.com/weekly-webtips/next-js-on-the-server-side-notes-to-self-e2170dc331ff
Solution 2:
Yeah, in order to use CSP with Material-UI (and JSS), you need to use a nonce
.
Since you have SSR, I see 2 opts:
-
You can publish CSP header at server side using next-secure-headers package or even Helmet. I hope you find a way how to pass
nonce
from Next to the Material UI. -
You can publish CSP header in
nginx
config (how do you do it now) and generate 'nonce' by nginx even it works as reverse proxy. You need to havengx_http_sub_module
orngx_http_substitutions_filter_module
in nginx.
TL;DR; details how it works pls see in https://scotthelme.co.uk/csp-nonce-support-in-nginx/ (it's a little bit more complicated way then just to use$request_id
nginx var)
Solution 3:
Nextjs config supports CSP headers:
https://nextjs.org/docs/advanced-features/security-headers
Solution 4:
Its recommended practice to set Content Security Policy in the Headers instead of meta tags. In NextJS
you can set the CSP in headers by modifying your next.config.js
.
Here is an example of adding CSP headers.
// next.config.js
const { nanoid } = require('nanoid');
const crypto = require('crypto');
const generateCsp = () => {
const hash = crypto.createHash('sha256');
hash.update(nanoid());
const production = process.env.NODE_ENV === 'production';
return `default-src 'self'; style-src https://fonts.googleapis.com 'self' 'unsafe-inline'; script-src 'sha256-${hash.digest(
'base64'
)}' 'self' 'unsafe-inline' ${
production ? '' : "'unsafe-eval'"
}; font-src https://fonts.gstatic.com 'self' data:; img-src https://lh3.googleusercontent.com https://res.cloudinary.com https://s.gravatar.com 'self' data:;`;
};
module.exports = {
...
headers: () => [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: generateCsp()
}
]
}
]
};
Next Documentation: https://nextjs.org/docs/advanced-features/security-headers