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:

  1. 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.

  2. 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 have ngx_http_sub_module or ngx_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