新的 Sanitizer API 旨在构建一个强大的处理器,以便将任意字符串安全地插入到网页中。
应用会一直处理不可信的字符串,但要将这些内容安全地渲染为 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
allowAttributes
和 dropAttributes
属性接受属性匹配列表,即键为属性名称、值为目标元素列表或 *
通配符的对象。
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
通过 about://flags
或 CLI 选项启用
Chrome
Chrome 正在实现 Sanitizer API。在 Chrome 93 或更高版本中,您可以通过启用 about://flags/#enable-experimental-web-platform-features
标志来试用此行为。在较低版本的 Chrome Canary 和开发者渠道中,您可以通过 --enable-blink-features=SanitizerAPI
启用此功能,并立即试用。请参阅有关如何使用 flag 运行 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 West 创建的 Sanitizer API Playground:
参考
照片由 Unsplash 用户 Towfiqu barbhuiya 拍摄。