使用 Sanitizer API 安全地操控 DOM

新的 Sanitizer API 旨在构建一个强大的处理器,用于将任意字符串安全地插入页面。

Jack J
Jack J

应用始终会处理不受信任的字符串,但安全地将相应内容呈现为 HTML 文档的一部分并非易事。如果小心谨慎,就很容易在无意间造成恶意攻击者可利用的跨站脚本攻击 (XSS) 机会。

为了降低这种风险,新的 Sanitizer API 提案旨在构建一个强大的处理器,将任意字符串安全地插入网页。本文介绍此 API 并说明其用法。

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

转义用户输入

将用户输入、查询字符串、Cookie 内容等插入到 DOM 中时,必须正确转义这些字符串。应特别注意通过 .innerHTML 对 DOM 进行操作,其中未转义的字符串是 XSS 的典型来源。

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

如果您对上述输入字符串中的 HTML 特殊字符进行转义或使用 .textContent 将其展开,将不会执行 alert(0)。不过,由于用户添加的 <em> 也会按原样扩展为字符串,因此无法使用此方法来保留 HTML 中的文本装饰。

最佳操作不是转义,而是清理

清理用户输入

转义和清理之间的区别

转义是指将特殊的 HTML 字符替换为 HTML 实体

清理是指从 HTML 字符串中移除语义有害的部分(例如脚本执行)。

示例

在前面的示例中,<img onerror> 会导致错误处理程序执行,但如果移除了 onerror 处理程序,则可以在 DOM 中安全地将其展开,同时让 <em> 保持不变。

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

为了正确清理,有必要将输入字符串解析为 HTML,省略被视为有害的标记和属性,并保留无害标记和属性。

提议的 Sanitizer API 规范旨在以浏览器的标准 API 形式提供此类处理。

Sanitizer API

Sanitizer API 的使用方式如下:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

但是,{ sanitizer: new Sanitizer() } 是默认参数。因此可以如下所示。

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

值得注意的是,setHTML() 是在 Element 中定义的。作为 Element 的一个方法,要解析的上下文不言自明(在本例中为 <div>),解析会在内部执行一次,结果直接扩展到 DOM 中。

如需获取字符串形式的清理结果,您可以使用 setHTML() 结果中的 .innerHTML

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

通过配置进行自定义

Sanitizer API 默认配置为移除会触发脚本执行的字符串。不过,您也可以通过配置对象将自己的自定义内容添加到排错流程中。

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

以下选项用于指定清理结果应如何处理指定元素。

allowElements:排错程序应保留的元素的名称。

blockElements:排错程序应在保留子级时移除的元素的名称。

dropElements:排错程序应移除的元素的名称及其子级。

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

您还可以使用以下选项控制排错程序是允许还是拒绝指定的属性:

  • allowAttributes
  • dropAttributes

allowAttributesdropAttributes 属性需要“属性匹配列表”,该对象的键为属性名称,且值为目标元素列表或 * 通配符。

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements 是用于允许或拒绝自定义元素的选项。如果允许,元素和属性的其他配置仍然适用。

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

API Surface

与 DomPurify 对比

DOMPurify 是一个提供排错功能的知名库。Sanitizer API 和 DOMPurify 之间的主要区别是,DOMPurify 将清理的结果以字符串形式返回,而您需要通过 .innerHTML 将其写入 DOM 元素中。

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

当浏览器中未实现 Sanitizer API 时,DOMPurify 可以作为后备方案。

DOMPurify 实现有几个缺点。如果返回字符串,则 DOMPurify 和 .innerHTML 会对输入字符串进行两次解析。这种双重解析会浪费处理时间,但也可能因第二次解析的结果与第一次解析的结果不同而引发有趣的漏洞。

HTML 还需要解析上下文。例如,<td><table> 中有意义,但在 <div> 中有意义。由于 DOMPurify.sanitize() 仅接受字符串作为参数,因此必须猜测出解析上下文。

Sanitizer API 在 DOMPurify 方法的基础上进行了改进,旨在消除双重解析和阐明解析上下文的需求。

API 状态和浏览器支持

标准化过程中,正在讨论 Sanitizer API,而 Chrome 正实施此 API。

步骤 状态
1. 创建铺垫消息 完成
2. 创建规范草稿 完成
3. 收集反馈并对设计进行迭代 完成
4. Chrome 源试用 完成
5. 发布 打算在 M105 上发货

Mozilla:认为该方案值得设计原型,并且正在积极实施

WebKit:在 WebKit 邮寄名单中查看响应。

如何启用 Sanitizer API

浏览器支持

  • x
  • x
  • x

来源

通过 about://flags 或 CLI 选项启用

Chrome

Chrome 正在实施 Sanitizer API。在 Chrome 93 或更高版本中,您可以启用 about://flags/#enable-experimental-web-platform-features 标志来试用此功能。在早期版本的 Chrome Canary 版和开发者版中,您可以通过 --enable-blink-features=SanitizerAPI 启用并立即试用。请查看有关如何运行带标志的 Chrome 的说明

Firefox

Firefox 还以实验性功能的形式实现 Sanitizer API。如需启用,请在 about:config 中将 dom.security.sanitizer.enabled 标志设置为 true

功能检测

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

反馈

如果您试用此 API 并有任何反馈,我们非常期待。分享您对 Sanitizer API GitHub 问题的看法,并与规范作者和对此 API 感兴趣的人员讨论。

如果您发现 Chrome 实现中存在任何 bug 或意外行为,请提交 bug 进行报告。选择 Blink>SecurityFeature>SanitizerAPI 组件并分享详细信息,以帮助实现人员跟踪问题。

演示

如需查看 Sanitizer API 的实际应用,请访问 Mike WestSanitizer API Playground

参考编号


照片由 Towfiqu barbhuiya 拍摄,来自 Unsplash 用户。