返回博客

你的分析脚本,正是 Content-Security-Policy 上的那个漏洞

严格的 CSP 能堵住 XSS,但一个第三方分析标签又把它重新打开。本文解释为什么主机白名单和缺失的 SRI 会削弱你的策略,以及第一方追踪器如何修复它。

Content-Security-Policy 是抵御跨站脚本攻击最有效的单一防线。可一旦你加入一个第三方分析标签,往往就不得不在它上面凿出一个洞。而现实世界中大多数 CSP 绕过,恰恰就藏在这个洞里。

原因是结构性的,而非偶然。重监控型的分析脚本在设计上就是要在运行时从你无法掌控的域名加载更多代码。而严格的 CSP 存在的意义,正是要禁止这种行为。

主机白名单是信任脚本的薄弱方式

过去允许某个分析厂商的做法是主机白名单:

Content-Security-Policy: script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;

OWASP 已经不再推荐这种模式。白名单很容易被绕过:任何被允许的源,只要托管了一个 JSONP 端点、一个开放重定向,或用户上传的脚本,就会变成 XSS 的攻击向量。标签管理器让情况更糟,因为它们存在的目的就是从任意源注入更多脚本。

当前的领先实践是基于「每次响应一个 nonce」加上 strict-dynamic 构建的严格 CSP

Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none';

有了 strict-dynamic,浏览器会信任由一个已受信任的脚本所加载的脚本——而这恰恰是标签管理器所依赖的行为。这对厂商很方便,对你却很危险:一个被攻陷的标签,现在就隐式获得了加载任何东西的权限。

SRI 在最需要它的脚本上偏偏不起作用

**子资源完整性(Subresource Integrity)**让浏览器在执行之前,将取回的文件与一个加密哈希进行校验:

<script
  src="https://cdn.example.com/tracker.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

SRI 支持 sha256sha384sha512,浏览器在任何不匹配时都会拒绝该资源。对于一个被攻陷的 CDN,它是一道干净利落的防线——前提是面对静态、版本固定的文件

但它在分析脚本上彻底失效。厂商标签是被刻意设计成可变的:同一个 URL 下的文件会在厂商发布新功能时随之改变,因此一个固定的哈希会在下一次部署时让数据采集中断。实践中,团队往往恰恰在那些对页面影响范围最广的脚本上放弃了 SRI。2025 年 10 月对供应链风险的重新评定,正是出于这个原因,将不安全的 SRI 实现评为高危等级

Trusted Types 在 2026 年 2 月抬高了下限

问题中关于 DOM-XSS 的那一半,如今有了浏览器层面的答案。截至 Baseline 2026 年 2 月,Trusted Types 已在主流浏览器中可用。你通过以下方式启用强制:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default;

之后,像 Element.innerHTML 这样的 DOM sink 会拒绝原始字符串,只接受由已注册策略所生成的值:

const policy = trustedTypes.createPolicy("default", {
  createHTML: (input) => DOMPurify.sanitize(input),
});
el.innerHTML = policy.createHTML(userInput); // ok
el.innerHTML = userInput;                     // throws TypeError

这是货真价实的强防护。它同时也是遗留分析代码和标签管理器代码会例行违反的那种规则,因为这些脚本会向 innerHTML 写入内容,并把 <script> 节点当作纯字符串注入。打开 Trusted Types,往往意味着你得先关掉你的分析厂商。

第一方追踪器顺应严格策略,而不是与之对抗

当分析脚本足够小巧、自包含,并且由你自己的源提供服务时,这种冲突便消失了。一个不再加载任何其他代码的追踪器,既不需要 strict-dynamic,也不需要在你的白名单里放一个厂商主机:

Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script';

就这么简单。没有第三方源,没有 nonce 的扩权,也没有为标签管理器开的例外。

Monoid 的追踪器大约 2 KB,没有任何依赖,也从不调用任何 DOM-XSS sink。它不写入 innerHTML,不注入脚本,也不读取或设置 cookie 或存储。它每次页面浏览只向 /collect 发送一个 keepalive 请求,其余时间保持空闲:

fetch('/collect', {
  method: 'POST',
  body: JSON.stringify({ site_id, path, referrer, screen_w }),
  keepalive: true,
});

由于文件是静态的,你可以对它应用 SRI,而它永远不会因此中断——固定的哈希会一直有效,直到你自己选择更新脚本为止。又因为身份标识是一个单向的每日哈希 SHA-256(IP | UA | SALT_SECRET | YYYY-MM-DD),根本不存在哪怕一个宽松的 CSP 都可能泄露的跨站画像。

这一模式可以推广到分析之外:你从白名单里移除的每一个脚本,都意味着随之移除了一整类 XSS 绕过。最安全的第三方脚本,就是那个你根本不加载的脚本。

来源

Comments

Loading comments…