Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using CSP in NextJS, nginx and Material-ui(SSR)

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:

  • followed https://material-ui.com/styles/advanced/#next-js to enable SSR
    • https://github.com/mui-org/material-ui/tree/master/examples/nextjs
  • I looked at https://material-ui.com/styles/advanced/#how-does-one-implement-csp but I'm not sure how I can get nginx to follow the 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;
  }
}
like image 349
Clumsy-Coder Avatar asked Dec 17 '22 12:12

Clumsy-Coder


2 Answers

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
like image 104
Clumsy-Coder Avatar answered Dec 20 '22 02:12

Clumsy-Coder


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

like image 36
Subash Avatar answered Dec 20 '22 01:12

Subash