使用子资源完整性、CORS 和引荐来源网址政策保护外部子资源

从第三方 CDN 或外部源加载脚本、样式表、字体或图片时,请使用子资源完整性(HTML 中的 integrity 和 CSS url() 中的 integrity())验证其加密完整性,配置跨源凭据处理(crossorigin 和 cross-origin()),并限制传出 Referer 标头(referrerpolicy 和 referrer-policy())。如需了解文档级内容安全政策和出站连接许可清单,请参阅网络安全和使用连接许可清单限制出站网络连接。

1. 为不可变资源生成加密完整性摘要

子资源完整性 (SRI) 可确保浏览器仅在外部资源的字节与应用声明的加密哈希匹配时,才会执行或呈现该资源。

  • 在构建或发布时计算摘要,切勿根据不可信的实时响应计算摘要:在构建或发布步骤中,根据已知良好的本地资源字节生成完整性哈希(或复制软件包作者发布的官方摘要),并将其硬编码到 HTML、CSS 或 JavaScript 中。切勿在运行时在浏览器中提取第三方网址来计算其自身的 integrity 哈希值,因为这会信任远程服务器返回的任何字节,从而使 SRI 失效。
  • 默认使用 sha384-:使用 sha384-(推荐的默认设置)、sha512- 或 sha256- 将完整性元数据格式设置为 <algorithm>-<base64-digest>。
  • 将外部子资源固定到不可变的、带版本号的网址:始终使用不可变的、固定版本或内容寻址的网址(例如 https://cdn.example.com/ui@2.4.0/app.min.js)引用第三方资源,而不是使用可变的浮动网址 (latest/app.min.js),因为浮动网址会使您的网站面临供应链发生静默更改的风险,并且一旦远程文件更新,SRI 就会失效。如果您必须加载可变且会就地更新的第三方脚本(例如供应商分析代码),请省略该网址上的 integrity,并改为使用内容安全政策(strict-dynamic/随机数)或 iframe 沙盒对其进行限制。
  • 当端点可能提供多个有效载荷时,请使用空格分隔多个摘要:所有支持 SRI 的浏览器都会实现 sha256-、sha384- 和 sha512-。如果某个网址可能合法地返回多个已知良好的载荷之一(例如在 A/B 测试、分阶段 CDN 发布或内容协商构建期间),请列出同一算法的多个摘要,并用空格分隔(例如 integrity="sha384-variantA... sha384-variantB...")。如果响应与所列出的最强算法的任何摘要匹配,浏览器就会接受该响应。
  • 使用 CORS 标头提供跨域子资源:只有当跨域响应可被 CORS 读取时,浏览器才能验证其完整性(对于匿名请求,使用 Access-Control-Allow-Origin: *;对于凭证请求,使用 Access-Control-Allow-Credentials: true 加上确切的请求来源)。
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
  const rawBytes =
    typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
  const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
    String.fromCharCode(b),
  ).join('');
  return `sha384-${btoa(binary)}`;
}

2. 安全的 HTML 脚本、样式表和预加载

将文档级 Referrer-Policy: strict-origin-when-cross-origin HTTP 响应标头(或在 <head> 中添加 <meta name="referrer" content="strict-origin-when-cross-origin">)设置为基准默认值,这样,任何省略了每个请求引荐来源网址修饰符的子资源或 CSS url() 仍会在跨域请求中剥离敏感路径和查询参数。

然后,在外部 <script>、<link rel="stylesheet">、<link rel="preload"> 和 <link rel="modulepreload"> 元素上,始终同时设置 integrity、crossorigin 和 referrerpolicy:

  • integrity="sha384-...":如果响应字节与预期摘要不匹配,则阻止执行或应用资源(对于被阻止的 <link rel="stylesheet">,linkElement.sheet 仍为 null,并且会触发 error 事件)。
  • crossorigin="anonymous"(建议对公共 CDN 资源使用的默认值)或 crossorigin="use-credentials":只要在跨域资源上设置了 integrity,就必须使用此值。省略 crossorigin 会导致浏览器以 no-cors 模式提取资源(产生无法进行哈希验证的不透明响应),并屏蔽该资源。
  • referrerpolicy="no-referrer"(第三方 CDN 的推荐默认值)或 referrerpolicy="strict-origin-when-cross-origin":替换文档引荐来源网址政策,以防止在 Referer 请求标头中向第三方主机泄露任何来源或路径信息。
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/ui@2.4.0/styles.css"
  integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
/>

<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
  src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

使用 fetch() 以编程方式提取外部子资源时,请传递匹配的 integrity、mode、credentials 和 referrerPolicy 选项(摘要不匹配会拒绝 promise,并返回 TypeError):

const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
  mode: 'cors',
  credentials: 'omit',
  referrerPolicy: 'no-referrer',
});

3. 使用 url() 修饰符保护 CSS 图片、字体和导入的样式表

CSS url() 请求修饰符(cross-origin()、referrer-policy() 和 integrity())可直接通过 CSS 控制 CSS 加载的资源(包括 background-image、content、mask-image、border-image-source、filter、@font-face src 和 @import 规则)的提取模式、引荐来源网址标头和子资源完整性验证:

修饰符函数 参数语法和允许的值 用途
cross-origin(<ident>) 未加引号的关键字:anonymous(推荐的默认值)或 use-credentials 使用 CORS 提取 CSS 资源,并控制是否发送凭据。在跨域 CSS 资源上,请始终与 integrity() 一起添加。
referrer-policy(<ident>) 不带引号的关键字:no-referrer(建议第三方素材资源使用的默认值)、strict-origin-when-cross-origin、origin、same-origin、strict-origin、origin-when-cross-origin、no-referrer-when-downgrade 或 unsafe-url 针对此特定 CSS 请求覆盖文档引荐来源网址政策。
integrity("<string>") 带英文引号的字符串:"sha384-<base64>"(或以空格分隔的摘要 "sha384-... sha384-...") 在呈现或应用提取的 CSS 资源之前,根据预期的加密摘要验证该资源。

在编写 url() 修饰符时,请遵循以下 CSS 语法和安全规则:

  • 用引号将 url("...") 内的网址字符串括起来:只有当 url(...) 内的网址是用引号括起来的 <string> 时,请求修饰符才有效(未用引号括起来的网址无法使用修饰符)。
  • 每个修饰符类型在每个 url() 中最多指定一次:您可以在 url("..." ...) 中以任意顺序组合 cross-origin()、referrer-policy() 和 integrity(),但重复使用同一修饰符类型两次(例如两个 integrity() 修饰符)是无效的。如需提供多个哈希,请在单个 integrity("sha384-... sha384-...") 字符串中用空格分隔它们。
  • 始终将跨域 integrity() 与 cross-origin(anonymous) 配对:虽然 @font-face src 和 mask-image 默认已在匿名 cors 模式下提取,但 background-image、content、border-image-source 和 @import 默认采用 no-cors 模式(会产生无法进行哈希验证的不透明响应),而外部 SVG filter 默认采用 same-origin 模式(会直接屏蔽跨域网址)。添加 cross-origin(anonymous)(或 cross-origin(use-credentials))可确保每个 CSS 属性的 CORS 验证保持一致。
  • 在 @import 和 @font-face 规则中,将修饰符放在 url("...") 内:将 cross-origin()、referrer-policy() 和 integrity() 放在 url("..." ...) 内,同时将 format("woff2") 和 tech(...) 等字体描述符放在 url(...) 外。
@import url(
  "https://cdn.example.com/theme@1.2.0/tokens.css"
  cross-origin(anonymous)
  referrer-policy(no-referrer)
  integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);

@font-face {
  font-family: "BrandSans";
  src: url(
    "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
  ) format("woff2");
  font-display: swap;
}

.verified-graphic {
  background-image: url(
    "https://cdn.example.com/media@1.0.0/badge.svg"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
  );
}

后备策略

如果您的基准目标尚不支持 CSS url() 请求修饰符(cross-origin()、referrer-policy() 或 integrity()),请将文档级政策与级联 CSS 声明结合使用,以便子资源保持正常运行并受到保护:

  1. 如果需要跨浏览器 SRI,请优先使用 HTML <link rel="stylesheet">,而不是 CSS @import:由于 <link rel="stylesheet"> 上的 subresource-integrity 和 referrer-policy 在所有浏览器中都广泛可用,因此只要必须在每个浏览器中强制执行完整性验证,就应使用 <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> 而不是 @import 加载外部样式表。
  2. 依赖第 2 步中的文档级 Referrer-Policy 标头或 <meta name="referrer">:在不支持 CSS url() 中的 referrer-policy() 的浏览器中,文档级 Referrer-Policy: strict-origin-when-cross-origin 标头或 <meta name="referrer" content="strict-origin-when-cross-origin"> 元素会自动从跨域 CSS 请求中剥离敏感路径和查询参数。
  3. 级联单独的 CSS 声明,以实现渐进增强:无法识别 url("..." ...) 内的修饰符的浏览器会在 CSS 解析时舍弃整个声明。首先声明一个普通 url("..."),然后声明 cross-origin() 和 referrer-policy()(适用于支持这两个修饰符但不支持 integrity() 的浏览器),最后声明包含 integrity("sha384-...") 的完整声明:

    • 切勿将未经检验的 url() 放在同一个以逗号分隔的 @font-face src 列表中:在单个 src 描述符中写入 src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); 会在运行时绕过子资源完整性。如果 CDN 提供的字体被篡改,导致 integrity() 检查失败,浏览器会将其视为加载失败,并自动回退到以逗号分隔的第二个 url("font.woff2")。使用单独的 src: 声明可确保支持的浏览器在 CSS 解析时覆盖未经验证的声明,并在运行时阻止篡改的字体。
    @font-face {
      font-family: "BrandSans";
      /* 1. Parse-time fallback for browsers without url() modifier support */
      src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2");
      /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      ) format("woff2");
      /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
      ) format("woff2");
      font-display: swap;
    }
    
    .verified-graphic {
      background-image: url("https://cdn.example.com/media@1.0.0/badge.svg");
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      );
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
      );
    }
    
  4. 使用 CSS.supports() 在 JavaScript 中检测 CSS url() 修饰符支持:

export function getCssUrlModifierSupport() {
  const supportsCrossOriginAndReferrer =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
    );

  const supportsUrlIntegrity =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
    );

  return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}