在 NextJS、nginx 和 Material-ui(SSR) 中使用 CSP
Using CSP in NextJS, nginx and Material-ui(SSR)
TLDR:我在使用 Material-UI(服务器端渲染)并由 Nginx 提供服务(使用反向代理)为 NextJS 设置 CSP 时遇到问题。
目前我在加载 Material-UI 样式表和加载我自己的样式时遇到问题
使用 @material-ui/core/styles
中的 makeStyles
注意:
- 已关注 https://material-ui.com/styles/advanced/#next-js 以启用 SSR
- 我查看了 https://material-ui.com/styles/advanced/#how-does-one-implement-csp 但我不确定如何让 nginx 遵循
nonce
值,因为随机数是作为不可预测的字符串生成的。
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;
}
}
是的,为了将 CSP 与 Material-UI(和 JSS)一起使用,您需要使用 nonce
。
因为你有SSR,我看到2个选项:
您可以使用 next-secure-headers 包甚至头盔在服务器端发布 CSP header。我希望你找到一种方法如何将 nonce
从 Next 传递到 Material UI.
您可以在 nginx
配置中发布 CSP header(您现在是怎么做的)并生成 'nonce' by nginx 即使它作为反向代理工作。你需要在 nginx 中有 ngx_http_sub_module
或 ngx_http_substitutions_filter_module
。
长话短说;博士;详细信息请参阅 https://scotthelme.co.uk/csp-nonce-support-in-nginx/(比使用 $request_id
nginx var 稍微复杂一点)
我找到的解决方案是在_document.tsx
中为内联js和css添加nonce值
_document.tsx
使用 uuid v4 生成随机数并使用 crypto nodejs 模块将其转换为 base64。
然后创建内容安全策略并添加生成的随机数值。
创建一个函数来完成创建随机数并生成 CSP 和 return CSP 字符串以及随机数
在HTML头部添加生成的CSP并添加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`),
// 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 配置
确保删除有关内容安全策略的添加 header。它可能会覆盖 _document.jsx 文件中的 CSP。
替代解决方案
正在创建自定义服务器并注入可在 _document.tsx
中访问的随机数和内容安全策略
Nextjs 配置支持 CSP headers:
建议的做法是在 Headers 而不是元标记中设置内容安全策略。在 NextJS
中,您可以通过修改 next.config.js
.
在 headers 中设置 CSP
这里是添加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()
}
]
}
]
};
下一个文档:https://nextjs.org/docs/advanced-features/security-headers
客户端呈现应用程序的解决方案
使用中间件和 getInitialProps 完成此操作。您只需对 <Head>{...}</Head>
进行 SSR 操作即可。
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;
TLDR:我在使用 Material-UI(服务器端渲染)并由 Nginx 提供服务(使用反向代理)为 NextJS 设置 CSP 时遇到问题。
目前我在加载 Material-UI 样式表和加载我自己的样式时遇到问题
使用 @material-ui/core/styles
makeStyles
注意:
- 已关注 https://material-ui.com/styles/advanced/#next-js 以启用 SSR
- 我查看了 https://material-ui.com/styles/advanced/#how-does-one-implement-csp 但我不确定如何让 nginx 遵循
nonce
值,因为随机数是作为不可预测的字符串生成的。
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;
}
}
是的,为了将 CSP 与 Material-UI(和 JSS)一起使用,您需要使用 nonce
。
因为你有SSR,我看到2个选项:
您可以使用 next-secure-headers 包甚至头盔在服务器端发布 CSP header。我希望你找到一种方法如何将
nonce
从 Next 传递到 Material UI.您可以在
nginx
配置中发布 CSP header(您现在是怎么做的)并生成 'nonce' by nginx 即使它作为反向代理工作。你需要在 nginx 中有ngx_http_sub_module
或ngx_http_substitutions_filter_module
。
长话短说;博士;详细信息请参阅 https://scotthelme.co.uk/csp-nonce-support-in-nginx/(比使用$request_id
nginx var 稍微复杂一点)
我找到的解决方案是在_document.tsx
中为内联js和css添加nonce值_document.tsx
使用 uuid v4 生成随机数并使用 crypto nodejs 模块将其转换为 base64。 然后创建内容安全策略并添加生成的随机数值。 创建一个函数来完成创建随机数并生成 CSP 和 return CSP 字符串以及随机数
在HTML头部添加生成的CSP并添加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`),
// 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 配置
确保删除有关内容安全策略的添加 header。它可能会覆盖 _document.jsx 文件中的 CSP。
替代解决方案
正在创建自定义服务器并注入可在 _document.tsx
中访问的随机数和内容安全策略Nextjs 配置支持 CSP headers:
建议的做法是在 Headers 而不是元标记中设置内容安全策略。在 NextJS
中,您可以通过修改 next.config.js
.
这里是添加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()
}
]
}
]
};
下一个文档:https://nextjs.org/docs/advanced-features/security-headers
客户端呈现应用程序的解决方案
使用中间件和 getInitialProps 完成此操作。您只需对 <Head>{...}</Head>
进行 SSR 操作即可。
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;