【问题标题】:Using CSP in NextJS, nginx and Material-ui(SSR)在 NextJS、nginx 和 Material-ui(SSR) 中使用 CSP
【发布时间】:2021-04-09 13:39:49
【问题描述】:

TLDR:我在使用 Material-UI(服务器端渲染)为 NextJS 设置 CSP 并由 Nginx 提供服务(使用反向代理)时遇到问题。

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

使用来自@material-ui/core/stylesmakeStyles

注意:

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

【问题讨论】:

    标签: nginx material-ui next.js content-security-policy


    【解决方案1】:

    是的,为了将 CSP 与 Material-UI(和 JSS)一起使用,您需要使用 nonce

    既然你有 SSR,我看到 2 个选择:

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

    2. 您可以在 nginx 配置中发布 CSP 标头(您现在如何操作)并生成 'nonce' by nginx,即使它可以用作反向代理。您需要在 nginx 中有 ngx_http_sub_modulengx_http_substitutions_filter_module
      TL;博士;详细说明它是如何工作的,请参阅https://scotthelme.co.uk/csp-nonce-support-in-nginx/(这比使用$request_id nginx var 要复杂一些)

    【讨论】:

      【解决方案2】:

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

      _document.tsx

      使用 uuid v4 生成一个 nonce,并使用 crypto nodejs 模块将其转换为 base64。 然后创建 Content Security Policy 并添加生成的 nonce 值。 创建一个函数来完成创建一个nonce并生成CSP并将CSP字符串与nonce一起返回

      在 HTML Head 中添加生成的 CSP 并添加元标记。

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

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

      nginx 配置

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


      替代解决方案

      创建自定义服务器并注入可在 _document.tsx 中访问的 nonce 和内容安全策略

      【讨论】:

        【解决方案3】:

        【讨论】:

          【解决方案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

          【讨论】:

          • 谢谢。但是如何将该哈希应用于样式和脚本标签?
          【解决方案5】:

          客户端渲染应用解决方案

          通过中间件和 getInitialProps 来实现这一点。你只需要 SSR &lt;Head&gt;{...}&lt;/Head&gt; 就可以了。

          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;
          

          【讨论】:

            猜你喜欢
            • 2020-08-14
            • 2023-03-22
            • 2020-07-07
            • 2022-10-17
            • 2020-10-08
            • 1970-01-01
            • 2021-06-13
            • 2021-07-27
            • 2021-03-24
            相关资源
            最近更新 更多