Menu

内容安全策略

内容安全策略 (CSP) 对于保护你的 Next.js 应用程序免受各种安全威胁非常重要,如跨站脚本攻击 (XSS)、点击劫持和其他代码注入攻击。

通过使用 CSP,开发者可以指定哪些来源可以用于内容源、脚本、样式表、图像、字体、对象、媒体 (音频、视频)、iframe 等。

示例

Nonce

Nonce 是一个唯一的、随机生成的字符串,用于一次性使用。它与 CSP 结合使用,可以选择性地允许某些内联脚本或样式执行,从而绕过严格的 CSP 指令。

为什么使用 nonce?

尽管 CSP 旨在阻止恶意脚本,但在某些情况下内联脚本是必要的。在这种情况下,nonce 提供了一种方法,允许这些脚本在具有正确 nonce 的情况下执行。

使用中间件添加 nonce

中间件允许你在页面渲染之前添加头部并生成 nonce。

每次查看页面时都应该生成一个新的 nonce。这意味着你必须使用动态渲染来添加 nonce

例如:

middleware.ts
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
`;
  // 替换换行符和空格
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, " ")
    .trim();
 
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
 
  requestHeaders.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue
  );
 
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
  response.headers.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue
  );
 
  return response;
}
middleware.js
import { NextResponse } from "next/server";
 
export function middleware(request) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
`;
  // 替换换行符和空格
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, " ")
    .trim();
 
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue
  );
 
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
  response.headers.set(
    "Content-Security-Policy",
    contentSecurityPolicyHeaderValue
  );
 
  return response;
}

默认情况下,中间件在所有请求上运行。你可以使用 matcher 来过滤中间件,使其仅在特定路径上运行。

我们建议忽略匹配预取 (来自 next/link) 和不需要 CSP 头部的静态资源。

middleware.ts
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下开头的路径:
     * - api (API 路由)
     * - _next/static (静态文件)
     * - _next/image (图像优化文件)
     * - favicon.ico (网站图标文件)
     */
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};
middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下开头的路径:
     * - api (API 路由)
     * - _next/static (静态文件)
     * - _next/image (图像优化文件)
     * - favicon.ico (网站图标文件)
     */
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};

读取 nonce

现在你可以使用 headers服务器组件 中读取 nonce:

app/page.tsx
import { headers } from "next/headers";
import Script from "next/script";
 
export default function Page() {
  const nonce = headers().get("x-nonce");
 
  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  );
}
app/page.jsx
import { headers } from "next/headers";
import Script from "next/script";
 
export default function Page() {
  const nonce = headers().get("x-nonce");
 
  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  );
}

不使用 Nonce

对于不需要 nonce 的应用程序,你可以直接在 next.config.js 文件中设置 CSP 头部:

next.config.js
const cspHeader = `
    default-src 'self';
    script-src 'self' 'unsafe-eval' 'unsafe-inline';
    style-src 'self' 'unsafe-inline';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
`;
 
module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value: cspHeader.replace(/\n/g, ""),
          },
        ],
      },
    ];
  },
};

版本历史

我们建议使用 Next.js 的 v13.4.20+ 版本来正确处理和应用 nonce。