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