在NextJS、nginx和Material-ui(SSR)中使用CSP

12

TLDR: 我在使用Material-UI(服务器端渲染)和Nginx(使用反向代理)为NextJS设置CSP时遇到了麻烦。

目前我遇到了加载Material-UI样式表和加载自己的样式的问题。

使用@material-ui/core/styles中的makeStyles

注意:

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;
  }
}

我们现在有关于处理随机数和内容安全策略的官方文档:https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy - leerob
5个回答

13
我找到的解决方案是在_document.tsx中为内联js和css添加nonce值。

_document.tsx

使用uuid v4生成一个nonce,并使用crypto nodejs模块将其转换为base64。 然后创建内容安全策略并添加生成的nonce值。 创建一个函数来完成创建nonce和生成CSP,并返回CSP字符串以及nonce。

将生成的CSP添加到HTML头部并添加meta标签。

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`),
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()],
  };
};

源代码: https://github.com/vercel/next.js/blob/master/examples/with-strict-csp/pages/_document.js

nginx配置

请确保删除有关内容安全策略的头信息添加。它可能会覆盖_document.jsx文件中的CSP。


替代方案

创建一个自定义服务器,并注入一次性数字和内容安全策略,可以在_document.tsx中访问


4

建议将内容安全策略设置在标头中而不是元标记中。在NextJS中,您可以通过修改next.config.js来在标头中设置CSP。

以下是添加CSP标头的示例。

// 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()
        }
      ]
    }
  ]
};

下一份文档:https://nextjs.org/docs/advanced-features/security-headers

6
谢谢。但是你如何将哈希应用到样式和脚本标签上? - Stephane

2

是的,如果要在Material-UI(和JSS)中使用CSP,则需要使用nonce

由于您有SSR,我看到了两个选择:

  1. 您可以使用next-secure-headers包或甚至是Helmet,在服务器端发布CSP标头。希望您能找到一种方法将nonce从Next传递给Material UI。

  2. 您可以在nginx配置中发布CSP标头(您现在如何做),并通过nginx生成'nonce',即使它作为反向代理也可以。您需要在nginx中拥有ngx_http_sub_modulengx_http_substitutions_filter_module
    TL;DR;详细信息请参见https://scotthelme.co.uk/csp-nonce-support-in-nginx/(这比仅使用$request_id nginx变量稍微复杂一些)


2

1

客户端渲染应用的解决方案

通过中间件和getInitialProps实现了这个功能。您只需要对<Head>{...}</Head>进行服务器端渲染即可。

pages/_middleware.js

import {NextResponse} from 'next/server';
import {v4 as uuid} from 'uuid';

function csp(req, res) {
  const nonce = `nonce-${Buffer.from(uuid()).toString('base64')}`;
  const isProduction = process.env.NODE_ENV === 'production';
  const devScriptPolicy = ['unsafe-eval']; // NextJS uses react-refresh in dev
  res.headers.append('Content-Security-Policy', [
    ['default-src', 'self', nonce],
    ['script-src',  'self', nonce].concat(isProduction ? [] : devScriptPolicy),
    ['connect-src', 'self', nonce],
    ['img-src', 'self', nonce],
    ['style-src', 'self', nonce],
    ['base-uri',  'self', nonce],
    ['form-action', 'self', nonce],
  ].reduce((prev, [directive, ...policy]) => {
    return `${prev}${directive} ${policy.filter(Boolean).map(src => `'${src}'`).join(' ')};`
  }, ''));
}

export const middleware = (req) => {
  const res = NextResponse.next();
  csp(req, res);
  return res;
}

pages/_app.js

import Head from 'next/head';

const DisableSSR = ({children}) => {
  return (
    <div suppressHydrationWarning>
      {typeof window === 'undefined' ? null : children}
    </div>
  );
}

const Page = ({ Component, pageProps, nonce }) => {
  return (
    <div>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta property="csp-nonce" content={nonce} />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <DisableSSR>
        <Component {...pageProps} />
      </DisableSSR>
    </div>
  );
}

Page.getInitialProps = async ({ctx: {req, res}}) => {
  const csp = {};
  res.getHeaders()['content-security-policy']?.split(';').filter(Boolean).forEach(part => {
    const [directive, ...source] = part.split(' ');
    csp[directive] = source.map(s => s.slice(1, s.length - 1));
  });
  return {
    nonce: csp['default-src']?.find(s => s.startsWith('nonce-')).split('-')[1],
  };
};

export default Page;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接