浏览器原生的 Sanitizer API 到底卡在哪一步?Firefox 158 把 HTML 清理从建完树再删挪到解析过程中

在 App 的 WebView 里渲染一条用户评论,你可能习惯性写 element.innerHTML = userInput,再补一句 DOMPurify——这套组合在移动端 H5 里跑了五年,但你大概没想过:危险内容其实先进了 DOM,清理器才追上去删。Firefox 158 Nightly 今天把顺序倒了过来,HTML 清理挪进了解析器内部,<script> 和 onerror 根本不会进入 DOM。

Sanitizer API 是浏览器原生的 HTML 清理接口

XSS 防御最常见的写法有两个坑:直接赋值 innerHTML 是裸奔;引第三方清理库则要背一整套依赖(CDN 可用性、版本升级、类型定义)。浏览器其实早给了原生方案——HTML Sanitizer API,WHATWG HTML 标准成员,核心方法是 Sanitizer.sanitizeFor():

// 场景:WebView 内嵌 H5 页面渲染服务端返回的用户评论 HTML
const sanitizer = new Sanitizer();

const dirty = '<img src=x onerror="stealTokens()"><p>这条评论很不错</p>';

// 第二个参数指定插入的目标元素类型,返回已清理的元素节点
const clean = sanitizer.sanitizeFor('div', dirty);

console.log(clean.innerHTML);
// → "<p>这条评论很不错</p>"(img 的 onerror 属性已被移除)

不引库、不发 CDN 请求、不用手工维护 allowedTags 黑名单——但这里有个关键问题:清理发生在什么时候?

旧行为:危险内容先进 DOM,再清理

Firefox 158 之前(包括 Chrome 105+ / Safari 16.4+ / Firefox 120+ 的现状),Sanitizer API 走的是 reified tree sanitization——先把完整 DOM 树建好,再回头清理:

用户输入: '<img src=x onerror=alert(1)>'
         ↓
HTML Parser 解析,构建完整 DOM 树(onerror 已经挂上)
         ↓
Sanitizer API 遍历树,移除危险节点/属性
         ↓
渲染已清理的树

两个实际后果:

  1. declarative shadow DOM 清不到。 <template shadowrootmode="open"> 在解析时就直接生成 shadow root,reified tree 阶段的清理器看不到 shadow root 内部——危险内容躲在影子树里,等于没有防护。
  2. Scoped Custom Element Registries 清不到。 Firefox 156 引入的独立 CustomElementRegistry() 注册的自定义元素,其构造与解析绑定,事后遍历树无法感知。

这正是移动端 WebView 场景的盲区:Hybrid 应用里大量使用 Web Components 封装 UI,用户内容一旦带 declarative shadow DOM,旧版 Sanitizer 形同虚设。

新行为:解析过程中实时清理,危险内容根本不进入 DOM

Firefox 158 Nightly(2026-09-10 起,flag:dom.security.sanitizer.while-parsing,Bug 2070497 已 RESOLVED FIXED)改为 parse-time sanitization:

用户输入: '<img src=x onerror=alert(1)>'
         ↓
HTML Parser 解析,内置 Sanitizer 实时拦截
         ↓
危险属性直接不插入树
         ↓
直接渲染(树里从未出现过 onerror)

三项关键差异:

  • 危险内容进不进 DOM:旧行为先插入再移除;新行为根本不插入
  • declarative shadow DOM:旧行为不可达;新行为在解析时同步拦截
  • Scoped Custom Element Registries:旧行为不可达;新行为随解析注册一并覆盖

顺序一变,安全模型就从”事后补救”变成了”事中防御”——不存在”危险节点存在过”的中间态,也就没有了清理前那一瞬间的可乘之机。

实战:从 DOMPurify 过渡到原生 Sanitizer

// —— before:依赖第三方库(当前生产推荐写法)——
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(userCommentHtml);
element.innerHTML = cleanHtml;

// —— after:原生 Sanitizer API(Firefox 158+ 支持解析时清理)——
const sanitizer = new Sanitizer({
  allowElements: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'img'],
  dropElements: ['script', 'style', 'iframe', 'object', 'embed'],
  allowAttributes: {
    'href': ['a'],
    'src': ['img'],
    'alt': ['img'],
  },
  dropAttributes: {
    'onerror': ['*'],
    'onclick': ['*'],
    'onload': ['*'],
  },
});

// sanitizeFor 返回元素节点,直接挂载,不再走 innerHTML 字符串拼接
const cleanNode = sanitizer.sanitizeFor('div', userCommentHtml);
commentList.appendChild(cleanNode);

注意 before 写法里 innerHTML = 字符串 是二次解析——浏览器要对同一份 HTML 解析两次(一次在库内,一次在赋值时)。after 返回节点直接 appendChild,解析只发生一次,这也是 parse-time 路径更顺的结构性原因。

兼容现状(2026 年 10 月):

  • Chrome 105+:基础 Sanitizer API
  • Safari 16.4+:基础 Sanitizer API
  • Firefox 120+:基础 Sanitizer API
  • Firefox 158 Nightly+:parse-time sanitization(本次新增,flag 默认开)

Chrome 尚未实现 parse-time 模式,但标准路径已明确。对 WebView 场景来说还有个放大效应:Android WebView 跟随 Chrome 版本节奏,Chrome 落地后,新版系统 WebView 会同步拿到这个能力。

为什么这个 API 拖了五年才真正可用

第三方依赖的惯性:DOMPurify 有 7000+ GitHub stars、完善的 TypeScript 类型、全球 CDN 覆盖,团队用了五年,没人敢换。

浏览器实现不一致:早期 Safari 的 Sanitizer 实现出过多个绕过方法,Chrome 早期行为也和规范有偏差,开发者不敢把它当唯一防线。

解析器改造成本高:parse-time 清理要求 HTML Parser 和 Sanitizer 深度耦合,动的是解析器核心逻辑。Mozilla 这次先以 Nightly flag(dom.security.sanitizer.while-parsing)落地,再走 bug 2070498 推默认启用,节奏是刻意放缓的。

下一步:这周就能做的三件事

  1. 先做对比测试:在非关键功能里用原生 new Sanitizer() 替换一处 DOMPurify 调用,对比 sanitizeFor() 的输出和你的白名单配置是否一致,把差异记下来。
  2. 排查 declarative shadow DOM 盲区:搜一遍项目里的 shadowrootmode——这些组件内的用户内容,现有 Sanitizer 方案根本覆盖不到,是真实的风险点。
  3. 跟踪 Chrome 落地:在 Chrome Platform Status 搜 Sanitizer,parse-time 支持一旦进 Beta,Android WebView 就跟着受益,届时再决定是否把原生 API 升级为默认路径。

在 Chrome 跟上之前,生产环境继续用 DOMPurify 是理性的;但解析时清理这条标准路径已经打通,原生方案取代第三方库只是时间问题。


参考:Bugzilla #2070497(parse-time 实现)、Bugzilla #2070498(默认启用)、Firefox 158 Nightly Release Notes、MDN HTML Sanitizer API、WHATWG HTML Standard。

评论区

0 条评论

登录后可评论。

阿跨·跨端开发 10 阅读