你以为 XSS 只能靠 DOMPurify?今天 Firefox 148 把这件事从 C++ 层原生化了
innerHTML 一直是 XSS 的头号来源。尽管安全社区喊了十几年「不要用 innerHTML」,但它太好用了——插入用户内容、渲染富文本、做 WYSIWYG 编辑器,全靠它。代价是每一行 innerHTML 都是一个潜在的注入点。
过去二十年,行业的主流解法是:写完 innerHTML 再套一层净化库。DOMPurify 是其中最流行的,压缩后约 24KB,每次页面加载都要多跑一段 JS 来弥补 innerHTML 的原生成本。库要维护、要更新、每次更新都是一次供应链事件。2025 年 Verizon DBIR 报告称 XSS 攻击仍占网页漏洞的 20%——说明这套模式并没有把问题解决干净。
根本原因是:这套安全逻辑一直跑在 JavaScript 层。
JavaScript 层的东西,再怎么做都有被绕过的可能。prototype pollution 攻击可以干扰 DOMPurify 的输出,DOM clobbering 可以篡改它的配置。攻击者不需要找到净化逻辑本身的漏洞,只需要找到绕过它的路径就行。
Firefox 148(2026 年 2 月 26 日)从根上改了这件事。它把 Sanitizer API 做进了浏览器引擎——不是 JavaScript,是 C++ 层。
用法简单到不敢相信:
const element = document.getElementById(‘output’);
element.setHTML(userInput);
一行替换,浏览器在解析 HTML 之前就把危险内容剥离了。script 标签、onerror/onclick 这类事件属性、javascript: 伪协议 URL,全部在 C++ 层被拦截,来不及进入 DOM,也来不及触发任何 JS 上下文。这不是黑名单逻辑,是浏览器自己的 HTML 解析器只往 DOM 里塞它认为安全的东西。
默认配置足够严苛,会移除:
所有脚本相关标签(script、noscript)
所有事件属性(onerror、onclick、onload 等)
javascript: 协议的 URL(href、src 等属性)
危险元素(object、embed、foreignObject 等)
如果你的业务真的需要放某些标签或属性,可以传配置:
const sanitizer = new Sanitizer({
allowElements: [‘p’, ‘b’, ‘i’, ’em’, ‘strong’, ‘a’],
allowAttributes: { ‘a’: [‘href’] }
});
element.setHTML(userInput, { sanitizer });
这个配置是在安全默认值之上再收紧,不是放开——就算配错了,也不会重新打开危险入口。
对于企业级场景,Firefox 148 还可以配合 Trusted Types 一起用,在架构层面锁定所有 HTML 注入点,实现零 innerHTML 泄漏的代码库。Mozilla 内部测试对已知 XSS payload 的拦截率达到 99%,每次解析增加的性能开销小于 1ms。
现在的问题是浏览器覆盖范围。Firefox 148 是第一个稳定版正式发货的浏览器。Chrome 和 Edge 还在实验阶段(chrome://flags/#sanitizer-api),Safari 还没开始实现。按照 Chrome 目前的发布节奏,setHTML() 可能在 2026 年内达到稳定。这段时间内,写法应该是渐进增强:
if (‘setHTML’ in Element.prototype) {
element.setHTML(untrusted);
} else {
element.innerHTML = DOMPurify.sanitize(untrusted);
}
对于还没升级的用户,DOMPurify 兜底;对于已经升级 Firefox 148 的用户,一步切到浏览器原生能力,24KB 的依赖可以直接从 bundle 里删掉。
下一步:翻出你的代码库,搜一下有多少处 innerHTML 把用户输入直接写进去。这些地方每一个都是潜在的注入点。把它们换成 setHTML(),就算 Chrome 还没稳定,你的代码也已经跑在最终会到来的标准上了。
supply chain 少一个依赖,security surface 少一大块。
评论区
登录后可评论。