← 中文资源

简体中文版本

带 Nonce 的 CSP 响应头构建器

基于安全默认值组装 Content-Security-Policy,合并自定义指令并注入每请求的 base64 nonce。

阅读英文原文

功能说明

该示例从一组安全默认指令出发构建 Content-Security-Policy 响应头,并支持追加自定义指令、切换为仅报告模式以及注入每次请求随机的 nonce。nonce 让内联脚本无需开启 unsafe-inline 也能执行,是缓解 XSS 的常用手段。构建结果可直接交给 Web 框架设置响应头。

关键代码

BASE_DIRECTIVES 默认只允许同源脚本、样式与图片,禁止 object 与 frame 嵌入。createNonce 用 crypto.getRandomValues 生成十六字节并转成 base64。buildCsp 校验 nonce 字符集后把它追加到 script-src,再遍历自定义指令,检查指令名只含小写字母、数字与连字符,值不能包含分号或空白。最后按指令顺序用分号连接,空数组指令会被过滤。

使用注意

nonce 必须每次响应重新生成,不能在多个请求间复用,也不能写进缓存页面。nonce 只对 script-src 生效,样式如需 nonce 要单独配置。不要同时开启 unsafe-inline,否则 nonce 的防护会被绕过。自定义指令会整体替换默认值,例如覆盖 img-src 时要显式保留需要允许的来源。仅报告模式不会拦截,只会上报违规。

验证方式

构建带 nonce 与自定义 connect-src 的头部,断言输出包含 nonce 值、指令以分号分隔且 img-src 被替换。测试 report-only 模式返回 Report-Only 头名。传入包含分号或空白的指令值、非法指令名与非 base64 nonce 分别抛错,并确认空数组指令不会出现在结果中。

代码示例

export type DirectiveValue = readonly string[];

export interface CspOptions {
  readonly directives?: Readonly<Record<string, DirectiveValue>>;
  readonly reportOnly?: boolean;
  readonly nonce?: string;
}

const BASE_DIRECTIVES: Record<string, readonly string[]> = {
  "default-src": ["'self'"],
  "script-src": ["'self'"],
  "style-src": ["'self'"],
  "img-src": ["'self'", "data:"],
  "object-src": ["'none'"],
  "base-uri": ["'self'"],
  "frame-ancestors": ["'none'"],
};

export function createNonce(): string {
  const bytes = new Uint8Array(16);
  globalThis.crypto.getRandomValues(bytes);
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary);
}

export function buildCsp(options: CspOptions = {}): { name: string; value: string } {
  const directives: Record<string, readonly string[]> = { ...BASE_DIRECTIVES };
  if (options.nonce !== undefined) {
    if (!/^[A-Za-z0-9+/]+={0,2}$/.test(options.nonce)) throw new Error("nonce must be base64");
    directives["script-src"] = [...(directives["script-src"] ?? []), `'nonce-${options.nonce}'`];
  }
  for (const [name, values] of Object.entries(options.directives ?? {})) {
    if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`invalid directive name "${name}"`);
    for (const value of values) {
      if (typeof value !== "string" || /[;\s]/.test(value)) {
        throw new Error(`invalid value for "${name}"`);
      }
    }
    directives[name] = values;
  }
  const serialized = Object.entries(directives)
    .filter(([, values]) => values.length > 0)
    .map(([name, values]) => `${name} ${values.join(" ")}`)
    .join("; ");
  return {
    name: options.reportOnly === true ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy",
    value: serialized,
  };
}

const header = buildCsp({
  nonce: createNonce(),
  directives: {
    "connect-src": ["'self'", "https://api.example.com"],
    "img-src": ["'self'", "data:", "https://cdn.example.com"],
  },
});
console.log(header.name);
console.log(header.value);
console.log(buildCsp({ reportOnly: true }).name);