新的 Sanitizer API 旨在构建一个强大的处理器,用于将任意字符串安全地插入到网页中。
应用经常需要处理不受信任的字符串,但要安全地将该内容呈现为 HTML 文档的一部分,可能非常棘手。如果不加注意,很容易意外地为恶意攻击者创造可乘之机,让他们能够进行跨站脚本攻击 (XSS)。
为了降低这种风险,新的 Sanitizer API 提案旨在构建一个强大的处理器,以便将任意字符串安全地插入到网页中。本文将介绍该 API,并说明其用法。
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())
转义用户输入
将用户输入、查询字符串、Cookie 内容等插入 DOM 时,必须正确转义这些字符串。应特别注意通过 .innerHTML 进行的 DOM 操作,其中未转义的字符串是 XSS 的典型来源。
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
$div.innerHTML = user_input
如果您对上述输入字符串中的 HTML 特殊字符进行转义,或使用 .textContent 对其进行扩展,则不会执行 alert(0)。不过,由于用户添加的 <em> 也会按原样展开为字符串,因此无法使用此方法来保留 HTML 中的文本装饰。
此处最好的做法不是转义,而是清理。
清理用户输入
转义和清理之间的区别
转义是指将特殊 HTML 字符替换为 HTML 实体。
清理是指从 HTML 字符串中移除语义上有害的部分(例如脚本执行)。
示例
在前面的示例中,<img onerror> 会导致执行错误处理程序,但如果移除了 onerror 处理程序,则可以在 DOM 中安全地展开 <img onerror>,同时保持 <em> 不变。
// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`
为了正确清理,有必要将输入字符串解析为 HTML,省略被认为有害的标记和属性,并保留无害的标记和属性。
提议的 Sanitizer API 规范旨在提供此类处理功能,作为浏览器的标准 API。
Sanitizer API
Sanitizer API 的使用方式如下:
const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div
不过,{ sanitizer: new Sanitizer() } 是默认实参。因此,它可以像下面这样。
$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>"/div
值得注意的是,setHTML() 是在 Element 上定义的。作为 Element 的一种方法,要解析的上下文是不言自明的(在本例中为 <div>),解析在内部完成一次,结果直接扩展到 DOM 中。
如需以字符串形式获取排错结果,您可以使用 setHTML() 结果中的 .innerHTML。
const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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" <]})> })
//< >divhe<ll><o bw>orld/b/div
$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div
您还可以使用以下选项控制清理器是否允许或拒绝指定属性:
allowAttributesdropAttributes
allowAttributes 和 dropAttributes 属性需要属性匹配列表,即键为属性名称、值为目标元素列表或 * 通配符的对象。
const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`
$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div
$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<
$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div
$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/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 >})
//< divcustom-e><lemh>ello/custom-elem/div
API Surface
与 DomPurify 的比较
DOMPurify 是一个提供清理功能的知名库。Sanitizer API 与 DOMPurify 之间的主要区别在于,DOMPurify 会以字符串形式返回清理结果,您需要通过 .innerHTML 将其写入 DOM 元素。
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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. 发布 | Intent to Ship on M105 |
Mozilla:认为此提案值得进行原型设计,并正在积极实现它。
WebKit:请参阅 WebKit 邮件列表中的回复。
如何启用 Sanitizer API
Browser Support
通过 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 West 提供的 Sanitizer API Playground:
参考
照片由 Towfiqu barbhuiya 拍摄,选自 Unsplash。