基于DOM的XSS一直是前端安全里最难缠的问题之一。它不像反射型XSS那样经过服务端,而是完全发生在浏览器内部:攻击者通过URL参数、location.hash、postMessage数据等渠道把恶意字符串送进页面,再由页面自己的JavaScript把这些字符串拼进innerHTML、eval之类的危险API,攻击就完成了。传统的防御手段比如服务端转义、WAF规则,对这类攻击经常无能为力,因为恶意载荷根本不经过服务端。这篇文章来聊聊Trusted Types这个相对较新的浏览器机制,以及它如何与边缘计算结合,构建一套更彻底的防御方案。

一、DOM XSS到底是怎么发生的
要理解Trusted Types的价值,先得弄清楚DOM XSS的完整链路。与传统的服务端注入不同,DOM XSS的数据流完全在客户端内部闭环:source(数据来源)到sink(危险汇聚点)之间的传递过程,服务端完全看不到。
典型的source包括location.hash、document.referrer、postMessage的message事件、localStorage里读取的数据等。典型的sink则是一批会把字符串当代码或HTML解析的API,常见的有:
// 常见的危险 sinks
element.innerHTML = userInput; // 把字符串当 HTML 解析
document.write(userInput); // 直接写入文档流
eval(userInput); // 把字符串当代码执行
element.setAttribute("onclick", userInput); // 注入事件处理器
setTimeout(userInput, 100); // 非函数形式会被 eval
script.src = userInput; // 动态加载脚本举个实际场景:某单页应用读取location.hash来做页面标题渲染,代码写成titleEl.innerHTML = decodeURIComponent(location.hash.slice(1))。攻击者只需要构造一个包含<img src=x onerror=alert(document.cookie)>的URL发给受害者,点击链接的瞬间恶意代码就在受害者浏览器里执行了。整个过程服务端日志里只有一次静态资源请求,WAF完全无法感知。
更麻烦的是,现代前端框架虽然大多默认做转义,但开发者总有绕过框架安全机制的时候:v-html、dangerouslySetInnerHTML、jQuery的html()方法,这些逃生舱口一旦接收到不受信任的输入,DOM XSS就回来了。
二、Trusted Types:从类型系统层面封堵危险入口
Trusted Types是Content Security Policy的一个扩展,目前已在Chromium内核浏览器中全面落地。它的核心思想非常直接:让危险的sink只接受特定类型的对象,拒绝普通字符串。也就是说,innerHTML不再接受任何字符串,只接受TrustedHTML类型的对象。普通字符串传进去,浏览器直接抛TypeError,攻击链在源头就被切断。
启用方式很简单,通过响应头或meta标签下发CSP策略:
<!-- 仅报告模式,用于摸底 -->
<meta http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script'; report-to csp-endpoint">
<lt;!-- 强制执行模式,需配合策略 -->
<meta http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script';
trusted-types default my-policy">这里有两个关键指令。require-trusted-types-for 'script'表示所有DOM XSS相关的sink都必须接收Trusted类型;trusted-types则声明页面允许创建哪些策略工厂。策略本身需要用JavaScript定义,作用是提供唯一合法的字符串清洗入口:
// 创建一个名为 my-policy 的策略
const escapePolicy = trustedTypes.createPolicy('my-policy', {
createHTML(input) {
// 白名单校验或调用 DOMPurify 清洗
return DOMPurify.sanitize(input);
},
createScriptURL(input) {
const url = new URL(input);
if (url.origin === 'https://cdn.ipipp.com') {
return url.toString();
}
throw new TypeError('不允许的脚本地址: ' + input);
}
});
// 现在这个调用是安全的
container.innerHTML = escapePolicy.createHTML(userInput);
// 而直接传字符串会抛出 TypeError
container.innerHTML = userInput; // 报错,攻击被阻断这种方式的最大优点是把分散在代码各处的转义逻辑收敛到一处。过去你需要祈祷每个开发者都记得escape,现在只需要保证策略函数写得足够严谨。策略还可以声明为sealed,禁止运行时再创建同名策略,防止攻击者通过注入脚本自行创建宽松策略来绕过限制。
落地时的建议路径是:先开report-only模式跑一段时间,通过上报接口收集所有违规调用点,逐一修复后再切换到强制模式。对于存量代码庞大、第三方脚本众多的站点,这个过渡期必不可少。
三、边缘计算在防御体系中的角色
Trusted Types解决了浏览器端的最后一道防线,但策略下发本身依赖HTTP响应头,而且大量页面内容现在通过边缘节点就近分发。这就是边缘计算的切入点:把安全策略的注入、违规报告的收集、甚至部分清洗逻辑都下沉到边缘节点执行,让防御贴近用户。
具体来说,边缘Worker可以在响应返回给用户的路径上动态改写CSP头。相比在源站每个应用都配置一遍,边缘侧统一注入有几点明显优势:第一,配置集中管理,改一次策略全网生效,不需要逐个应用发版;第二,可以做灰度控制,按IP段、用户比例、地域逐步开启强制模式;第三,违规报告先汇聚到边缘,在边缘做聚合去重再回传,避免报告洪峰打垮源站的报告收集接口。
一个典型的边缘Worker逻辑大致如下:
// 边缘 Worker 示例:注入 CSP 并拦截违规报告洪峰
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// 拦截违规报告,聚合后批量回传
if (url.pathname === '/csp-report') {
const body = await request.json();
await aggregateAndStore(body); // 边缘KV聚合,减少回源压力
return new Response(null, { status: 204 });
}
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set('Content-Security-Policy',
"require-trusted-types-for 'script'; " +
"trusted-types default sanitize-policy; " +
"report-to csp-endpoint; " +
"report-uri /csp-report");
headers.set('Report-To',
'{"group":"csp-endpoint","endpoints":[{"url":"https://edge.ipipp.com/csp-report"}]}');
return new Response(response.body, { status: response.status, headers });
}除了注入头,边缘节点还能承担预清洗职责。对于必须透传用户内容的场景(比如评论预览接口),可以在边缘调用轻量级清洗逻辑,把明显含恶意标签的请求在到达源站前就拒绝掉。这样形成了双层防线:边缘负责粗筛和策略管控,浏览器端Trusted Types负责最终兜底。即使边缘规则被新型绕过手法穿透,浏览器端的类型检查依然会把字符串拦在sink之外。
四、落地时的注意事项
首先是兼容性。Trusted Types在Firefox和Safari中尚未完全普及,所以不能把它当作唯一防线,输出转义、框架默认安全机制仍要保留。可以通过特性检测window.trustedTypes判断支持情况,并在不支持的环境下回退到严格的转义工具链。
其次是策略函数本身要极其小心。createHTML的实现如果只是简单过滤<script>标签,很容易被编码变换、嵌套构造绕过。强烈建议直接使用DOMPurify这类经过长期对抗打磨的库,而不是自己手写正则。同时策略名要收敛,只创建业务必需的策略,多余的策略工厂就是多余的攻击面。
最后是监控闭环。强制模式上线后,任何TypeError都意味着功能受损,违规报告则意味着有漏网的调用点。边缘侧应该把这两类事件都接入告警,区分攻击尝试和功能故障,持续迭代策略配置。安全从来不是一次性的配置,而是一个持续对抗的过程。
边缘计算Trusted TypesDOM XSS防御修改时间:2026-09-04 08:36:48