TLDR: I'm having trouble with setting up CSP for NextJS using Material-UI (server side rendering) and served by Nginx (using reverse proxy).
Currently I have issues with loading Material-UI stylesheet, and loading my own styles
using makeStyles from @material-ui/core/styles
NOTE:
nonce values, since nonce are generated as unpredictable string.default.conf (nginx)
# https://www.acunetix.com/blog/web-security-zone/hardening-nginx/
upstream nextjs_upstream {
server localhost:3000;
# We could add additional servers here for load-balancing
}
server {
listen $PORT default_server;
# redirect http to https. use only in production
# if ($http_x_forwarded_proto != 'https') {
# rewrite ^(.*) https://$host$request_uri redirect;
# }
server_name _;
server_tokens off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# hide how is app powered. In this case hide NextJS is running behind the scenes.
proxy_hide_header X-Powered-By;
# set client request body buffer size to 1k. Usually 8k
client_body_buffer_size 1k;
client_header_buffer_size 1k;
client_max_body_size 1k;
large_client_header_buffers 2 1k;
# ONLY respond to requests from HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
# to prevent click-jacking
add_header X-Frame-Options "DENY";
# don't load scripts or CSS if their MIME type as indicated by the server is incorrect
add_header X-Content-Type-Options nosniff;
add_header 'Referrer-Policy' 'no-referrer';
# Content Security Policy (CSP) and X-XSS-Protection (XSS)
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self' https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap ; form-action 'none'; frame-ancestors 'none'; base-uri 'none';" always;
add_header X-XSS-Protection "1; mode=block";
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
# limit request types to HTTP GET
# ignore everything else
limit_except GET { deny all; }
proxy_pass http://nextjs_upstream;
}
}
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With