你以为 innerHTML 只是代码规范问题?今天浏览器把这件事从根上强制执行了
你的项目里大概率躺着一条没人敢删的 el.innerHTML = someValue。
你说不清 someValue 从哪来——也许是模板字符串,也许是接口返回,也许是三年前一次 refactor 之后 location.hash 悄悄带进来的。那个没人愿意碰的组件里,这行代码就这么躺着,躺了十八个月。
这就是 DOM XSS 的本质:sink 接受的是明文字符串,而字符串长什么样浏览器根本分辨不了。
直到今天,浏览器不再分辨了——它直接拒收。
2026 年 2 月,Trusted Types 成了浏览器标配
Firefox 148 在今年二月补上了最后一块拼图,Trusted Types 自此覆盖 Chrome、Edge、Safari、Firefox 四大核心浏览器,正式进入 Baseline。
这意味着什么?一旦开启 enforcement 模式,下面这些写法会直接抛出 TypeError:
el.innerHTML = userInput; // ❌ TypeError
el.outerHTML = template; // ❌ TypeError
el.insertAdjacentHTML("beforeend", html); // ❌ TypeError
eval(userCode); // ❌ TypeError
new Function(scriptStr); // ❌ TypeError
scriptElement.src = url; // ❌ TypeError
不是警告,不是 console.error——是运行时异常,代码直接中断。
怎么用?先从 CSP 响应头说起
Trusted Types 通过 Content-Security-Policy 开启,两个指令配合使用:
Content-Security-Policy: require-trusted-types-for "script"; trusted-types my-policy;
require-trusted-types-for script:这就是开关,打开后所有危险 sink 只接受 Trusted* 类型值trusted-types my-policy;:白名单策略名,只有通过 createPolicy 注册的工厂函数才能生产可信值
注册一个策略是这样写的:
const escapePolicy = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => DOMPurify.sanitize(input),
createScriptURL: (url) => new URL(url, location.origin).href
});
// 之后
el.innerHTML = escapePolicy.createHTML(userInput); // ✅
el.innerHTML = userInput; // ❌ TypeError
关键在于:Trusted Types 本身不负责过滤,过滤逻辑全在你的 policy 里。API 只做一件事——把「所有字符串都能进 sink」收窄成「只有你显式声明的 policy 函数能进 sink」。
为什么这事值得专门说
看一下 Firefox 148 支持之前的浏览器覆盖图:Chrome 从 2020 年 v83 就有了,Safari 在 v26 支持,Edge 一直跟着 Chrome。就 Firefox 一直缺席,企业安全策略里开这个头等于把 Safari 和 Firefox 用户排除在外。
现在四大引擎全部就绪,这件事才真正从「Chrome 安全小技巧」变成「前端安全基础设施」。
另一个值得注意的信号是 Google 发布的生产落地案例:启用 enforcement 之后的项目,DOM XSS 漏洞数量降到了接近零。不是因为修了多少个 bug,而是整个攻击面被封死了。
上线三步走
第一步:开 Report-Only 模式,看哪些代码会爆
Content-Security-Policy-Report-Only: require-trusted-types-for "script"; report-uri /csp-violations
这个模式下不会报错,只会收集违规报告。在控制台和你的报表接口里同时收到:
// 浏览器控制台会输出违规详情
document.addEventListener("securitypolicyviolation", (e) => {
console.error("违规位置:", e.sourceFile, e.lineNumber);
console.error("违规片段:", e.sample);
});
第二步:逐个修复,替换 innerHTML
大部分违规的修复方式其实很简单——压根不需要 innerHTML:
// 之前
el.innerHTML = `<img src="${imgUrl}">`;
// 之后
el.appendChild(Object.assign(document.createElement("img"), { src: imgUrl }));
必须动态生成 HTML 的,用 DOMPurify 配合 Trusted Types:
el.innerHTML = DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true });
第三步:切 enforcement,上线
确认 Report-Only 没有异常流量之后,把 Report-Only 去掉,正式开启 enforce 模式。
几个真实问题
Q:第三方 SDK 爆了怎么办?
A:用一个叫 default 的保留策略名做兜底,它会捕获所有未匹配的 sink 调用,作为迁移期的临时出口。但记得这只是临时方案,别长期用。
Q:React / Vue 项目需要担心吗?
A:React 和 Vue 默认不走 innerHTML,走的是虚拟 DOM diff 之后写入 safe DOM API,所以大多数情况下不受影响。但如果用了 dangerouslySetInnerHTML 或 v-html,还是会触发违规。Angular 项目已经有官方支持。
Q:老浏览器不支持怎么办?
A:Trusted Types 是 graceful fallback——不支持的浏览器会忽略 CSP 指令,不报错,只是没有这层保护。所以可以放心全量开启,现代浏览器自动获得防护, legacy 环境不受影响。
你的下一步
- 今晚:在本地跑一下
Content-Security-Policy-Report-Only: require-trusted-types-for script看有没有违规输出 - 本周:扫一遍项目中所有 innerHTML / eval / document.write 的调用,评估哪些需要保留
- 本月:为保留动态 HTML 的模块创建 policy,上线 enforce 模式
这件事的本质变化是:DOM XSS 防护不再是代码规范层面的要求,而是浏览器层面的强制执行。你不需要靠 code review 拦住 innerHTML,浏览器直接帮你拦住。
评论区
登录后可评论。