从第三方 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-facesrc和mask-image默认已在匿名cors模式下提取,但background-image、content、border-image-source和@import默认采用no-cors模式(会产生无法进行哈希验证的不透明响应),而外部 SVGfilter默认采用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 声明结合使用,以便子资源保持正常运行并受到保护:
- 如果需要跨浏览器 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 步中的文档级
Referrer-Policy标头或<meta name="referrer">:在不支持 CSSurl()中的referrer-policy()的浏览器中,文档级Referrer-Policy: strict-origin-when-cross-origin标头或<meta name="referrer" content="strict-origin-when-cross-origin">元素会自动从跨域 CSS 请求中剥离敏感路径和查询参数。 级联单独的 CSS 声明,以实现渐进增强:无法识别
url("..." ...)内的修饰符的浏览器会在 CSS 解析时舍弃整个声明。首先声明一个普通url("..."),然后声明cross-origin()和referrer-policy()(适用于支持这两个修饰符但不支持integrity()的浏览器),最后声明包含integrity("sha384-...")的完整声明:- 切勿将未经检验的
url()放在同一个以逗号分隔的@font-facesrc列表中:在单个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") ); }- 切勿将未经检验的
使用
CSS.supports()在 JavaScript 中检测 CSSurl()修饰符支持:
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 };
}