1. 引言
本节为非规范性内容。
Web 应用经常需要在客户端处理 HTML 字符串,
可能作为客户端模板解决方案的一部分,也可能作为渲染用户生成内容的一部分等。要以安全方式做到这一点很困难。
将字符串简单拼接后塞入
Element
的
innerHTML
的朴素做法充满风险,因为它可能以多种出人意料的方式触发 JavaScript 执行。
诸如 [DOMPURIFY] 之类的库尝试 通过在插入前仔细解析并净化字符串、构建 DOM 并通过允许列表过滤其成员来管理这一问题。 但事实证明这是一种脆弱的方法,因为暴露给 Web 的解析 API 并不总能以合理方式映射到浏览器在“真实” DOM 中实际渲染字符串时的行为。 此外,这些库需要随时间跟进浏览器行为的变化;曾经安全的做法可能会因新的平台级特性而变成定时炸弹。
浏览器相当清楚自己何时会执行代码。我们可以通过教会浏览器如何以安全方式从任意字符串渲染 HTML 来改进用户空间库, 并以更有可能随着浏览器自身解析器实现变化而维护与更新的方式来实现。本文档概述了一个旨在完成此目标的 API。
1.1. 目标
-
通过为开发者提供处理用户控制的 HTML 的机制,减轻基于 DOM 的跨站脚本(XSS)攻击风险, 防止在注入时直接执行脚本。
-
使 HTML 输出在当前用户代理内使用是安全的,并考虑其对 HTML 的当前理解。
-
允许开发者覆盖默认的元素和属性集合。添加某些元素和属性可以防止 script gadget 攻击。
1.2. API 概览
Sanitizer API 提供了将包含 HTML 的字符串解析为 DOM 树的功能,并按用户提供的配置过滤生成的树。方法以两种风格成对提供:
-
安全与 不安全: “安全”方法不会生成任何会执行脚本的标记。也就是说,它们应当避免 XSS。“不安全”方法会按指示解析和过滤输入。 另见:§ 4 安全注意事项。
-
上下文:方法定义在
Element和ShadowRoot上,会替换这些Node的子节点,并在很大程度上类似于innerHTML。 还在Document上提供静态方法,用于解析整个文档,与DOMParser的parseFromString()大体类似。
2. 框架
2.1. 清理器 API
Element
接口定义了两个方法,setHTML()
和
setHTMLUnsafe()。
二者均接收包含 HTML 标记的 DOMString
与可选配置。
partial interface Element { [CEReactions ]undefined setHTMLUnsafe ((TrustedHTML or DOMString ),html optional SetHTMLUnsafeOptions = {}); };options
partial interface Element { [CEReactions ]undefined setHTML (DOMString ,html optional SetHTMLOptions = {}); };options
Element
的
setHTMLUnsafe(html, options)
方法步骤为:
-
令 compliantHTML 为调用 获取受信任类型兼容字符串 算法,传入
TrustedHTML、 this 的 相关全局对象、html、"Element setHTMLUnsafe" 和 "script" 后的结果。 -
若 this 为
template元素,则令 target 为其 template contents;否则为 this 本身。 -
设置并 过滤 HTML,给定 target、this、 compliantHTML、options 和 false。
Element
的
setHTML(html, options) 方法步骤为:
-
若 this 为
template, 则令 target 为其 template contents;否则为 this。 -
设置并 过滤 HTML,给定 target、this、html、 options 和 true。
partial interface ShadowRoot { [CEReactions ]undefined setHTMLUnsafe ((TrustedHTML or DOMString ),html optional SetHTMLUnsafeOptions = {}); };options
partial interface ShadowRoot { [CEReactions ]undefined setHTML (DOMString ,html optional SetHTMLOptions = {}); };options
这些方法也在 ShadowRoot
上提供:
ShadowRoot
的
setHTMLUnsafe(html, options)
方法步骤为:
-
令 compliantHTML 为调用 获取受信任类型兼容字符串 算法,并传入
TrustedHTML、 this 的 相关全局对象、html、"ShadowRoot setHTMLUnsafe" 和 "script" 后的结果。 -
设置并 过滤 HTML,使用 this、 this 的 shadow host(作为上下文元素)、 compliantHTML、options 和 false。
ShadowRoot
的
setHTML(html, options) 方法步骤为:
-
设置并 过滤 HTML,使用 this(作为目标)、this(作为上下文 元素)、 html、options 和 true。
Document
接口新增两个用于解析整个 Document
的方法:
partial interface Document {static Document parseHTMLUnsafe ((TrustedHTML or DOMString ),html optional SetHTMLUnsafeOptions = {}); };options
partial interface Document {static Document parseHTML (DOMString ,html optional SetHTMLOptions = {}); };options
parseHTMLUnsafe(html, options)
方法步骤为:
-
令 compliantHTML 为调用 获取受信任类型兼容字符串 算法,并传入
TrustedHTML、 当前全局对象、html、"Document parseHTMLUnsafe" 和 "script" 后的结果。 -
令 document 为一个新的
Document, 其 content type 为 "text/html"。注意:由于 document 没有浏览上下文,脚本被禁用。
-
将 document 的 allow declarative shadow roots 设为 true。
-
给定 document 与 compliantHTML,从字符串解析 HTML。
-
令 sanitizer 为使用 options 和 false 调用 根据选项获取一个 sanitizer 实例 的结果。
-
在 document 上以 sanitizer 和 false 调用 sanitize。
-
返回 document。
parseHTML(html, options) 方法步骤为:
-
令 document 为一个新的
Document, 其 content type 为 "text/html"。注意:由于 document 没有浏览上下文,脚本被禁用。
-
将 document 的 allow declarative shadow roots 设为 true。
-
给定 document 与 html,从字符串解析 HTML。
-
令 sanitizer 为使用 options 和 true 调用 根据选项获取一个 sanitizer 实例 的结果。
-
在 document 上以 sanitizer 和 true 调用 sanitize。
-
返回 document。
2.2. SetHTML 选项与配置对象。
一系列类似 setHTML()
的
方法都接收一个选项字典。当前仅定义了该字典的一个成员:
enum {SanitizerPresets };"default" dictionary { (SetHTMLOptions Sanitizer or SanitizerConfig or SanitizerPresets )= "default"; };sanitizer dictionary { (SetHTMLUnsafeOptions Sanitizer or SanitizerConfig or SanitizerPresets )= {}; };sanitizer
Sanitizer
配置对象封装了过滤配置。同一份配置可用于 “安全”
与“不安全” 方法,其中“安全”方法会对传入配置隐式执行
removeUnsafe
操作,并在未传入配置时使用默认配置。默认配置在“安全”与“不安全”方法间不同:“安全”方法以默认安全为目标,使用更严格的默认;而“不安全”方法默认不受限制。配置的使用意图是:在页面生命周期早期构建一份(或少数几份)配置,此后按需复用。这允许实现对配置进行预处理。
配置对象可被查询以返回配置字典,也可直接修改。
[Exposed =Window ]interface {Sanitizer constructor (optional (SanitizerConfig or SanitizerPresets )= "default"); // Query configuration:configuration SanitizerConfig get (); // Modify a Sanitizer's lists and fields:boolean allowElement (SanitizerElementWithAttributes );element boolean removeElement (SanitizerElement );element boolean replaceElementWithChildren (SanitizerElement );element boolean allowProcessingInstruction (SanitizerPI );pi boolean removeProcessingInstruction (SanitizerPI );pi boolean allowAttribute (SanitizerAttribute );attribute boolean removeAttribute (SanitizerAttribute );attribute boolean setComments (boolean );allow boolean setDataAttributes (boolean ); // Remove markup that executes script.allow boolean removeUnsafe (); };
Sanitizer
具有一个关联的 SanitizerConfig
configuration。
constructor(configuration)
方法步骤为:
get() 方法步骤如下:
-
令 config 为 this 的 configuration。
-
-
对于 config["
elements"] 里的每个 element:-
如果 element["
attributes"] 存在:-
令 element["
attributes"] 为对 element["attributes"] 调用 升序排序 的结果, 其中 attrA 小于项 attrB。
-
-
如果 element["
removeAttributes"] 存在:-
令 element["
removeAttributes"] 为对 element["removeAttributes"] 调用 升序排序 的结果, 其中 attrA 小于项 attrB。
-
-
-
令 config["
elements"] 为对 config["elements"] 调用 升序排序 的结果, 其中 elementA 小于项 elementB。
-
-
否则:
-
令 config["
removeElements"] 为对 config["removeElements"] 调用 升序排序 的结果, 其中 elementA 小于项 elementB。
-
-
如果 config["
replaceWithChildrenElements"] 存在:-
令 config["
replaceWithChildrenElements"] 为对 config["replaceWithChildrenElements"] 调用 升序排序 的结果, 其中 elementA 小于项 elementB。
-
-
如果 config["
processingInstructions"] 存在:-
令 config["
processingInstructions"] 为对 config["processingInstructions"] 调用 升序排序 的结果, 其中 piA["target"] 码元小于 piB["target"]。
-
-
否则:
-
令 config["
removeProcessingInstructions"] 为对 config["removeProcessingInstructions"] 调用 升序排序 的结果, 其中 piA["target"] 码元小于 piB["target"]。
-
-
如果 config["
attributes"] 存在:-
令 config["
attributes"] 为对 config["attributes"] 调用 升序排序 的结果, 其中 attrA 小于项 attrB。
-
-
否则:
-
令 config["
removeAttributes"] 为对 config["removeAttributes"] 调用 升序排序 的结果, 其中 attrA 小于项 attrB。
-
-
返回 config。
allowElement(element) 方法步骤如下:
-
是否存在全局允许列表或移除列表,以及
-
这些列表中是否已经包含 element。
-
令 configuration 为 this 的 configuration。
-
令 element 为 用 规范化带属性的 sanitizer 元素 处理 element 的结果。
-
如果 configuration["
elements"] 存在:-
令 modified 为对 configuration["
replaceWithChildrenElements"] 调用 remove element 的结果。 -
注释:我们需要确保单元素属性不会与全局属性重叠。
-
如果 configuration["
attributes"] 存在:-
如果 element["
attributes"] 存在:-
令 element["
attributes"] 为对 element["attributes"] 调用 去重 的结果。 -
令 element["
attributes"] 为 差集 的结果, 即 element["attributes"] 和 configuration["attributes"]。 -
如果 configuration["
dataAttributes"] 为 true:-
移除所有 item 项 从 element["
attributes"], 其中 item 是 自定义 data-属性。
-
-
-
如果 element["
removeAttributes"] 存在:-
令 element["
removeAttributes"] 为对 element["removeAttributes"] 调用 去重 结果。 -
令 element["
removeAttributes"] 为 交集 的结果, 即 element["removeAttributes"] 和 configuration["attributes"]。
-
-
-
否则:
-
如果 element["
attributes"] 存在:-
令 element["
attributes"] 为对 element["attributes"] 调用 去重 结果。 -
令 element["
attributes"] 为 差集 的结果, 即 element["attributes"] 和 element["removeAttributes"],默认为 « »。 -
移除 element["
removeAttributes"]。 -
令 element["
attributes"] 为 差集 的结果, 即 element["attributes"] 和 configuration["removeAttributes"]。
-
-
如果 element["
removeAttributes"] 存在:-
令 element["
removeAttributes"] 为对 element["removeAttributes"] 调用 去重 结果。 -
令 element["
removeAttributes"] 为 差集 的结果, 即 element["removeAttributes"] 和 configuration["removeAttributes"]。
-
-
-
注释:这是全局允许列表且已包含 element 的情况。
-
令 current element 为 configuration["
elements"] 中 item, 其中 item["name"] 等于 element["name"] 且 item["namespace"] 等于 element["namespace"]。 -
如果 element 等于 current element 则返回 modified。
-
返回 true。
-
-
否则:
-
如果 element["
attributes"] 存在 或 element["removeAttributes"] 默认为 « » 非 空:-
用户代理可以 向控制台报告警告,说明该操作不被支持。
-
返回 false。
-
-
令 modified 为对 configuration["
replaceWithChildrenElements"] 调用 remove element 的结果。 -
如果 configuration["
removeElements"] 不包含 element:-
注释:这是全局移除列表未包含 element 的情况。
-
返回 modified。
-
-
注释:这是全局移除列表包含 element 的情况。
-
移除 element 从 configuration["
removeElements"]。 -
返回 true。
-
replaceElementWithChildren(element)
方法的步骤为:
-
将 element 设为以 element 作为参数调用 规范化 sanitizer 元素 的结果。
-
如果 内置不可替换元素列表 包含 element:
-
返回 false。
-
-
如果 configuration["
replaceWithChildrenElements"] 包含 element:-
返回 false。
-
-
从 configuration["
removeElements"] 中移除 element。 -
添加 element 到 configuration["
replaceWithChildrenElements"]。 -
返回 true。
allowProcessingInstruction(pi)
方法步骤如下:
-
令 configuration 为 this 的 configuration。
-
令 pi 为用 规范化处理指令处理 pi 的结果。
-
如果 configuration["
processingInstructions"] 存在:-
如果 configuration["
processingInstructions"] 包含 pi:-
返回 false。
-
-
追加 pi 到 configuration["
processingInstructions"]。 -
返回 true。
-
-
否则:
-
如果 configuration["
removeProcessingInstructions"] 包含 pi:-
移除 configuration["
removeProcessingInstructions"] 中 "target" 等于 pi["target"] 的项。 -
返回 true。
-
-
返回 false。
-
removeProcessingInstruction(pi)
方法步骤如下:
-
令 configuration 为 this 的 configuration。
-
令 pi 为用 规范化处理指令处理 pi 的结果。
-
如果 configuration["
processingInstructions"] 存在:-
如果 configuration["
processingInstructions"] 包含 pi:-
移除 configuration["
processingInstructions"] 中 "target" 等于 pi["target"] 的项。 -
返回 true。
-
-
返回 false。
-
-
否则:
-
如果 configuration["
removeProcessingInstructions"] 包含 pi:-
返回 false。
-
-
追加 pi 到 configuration["
removeProcessingInstructions"]。 -
返回 true。
-
allowAttribute(attribute) 方法步骤如下:
-
令 configuration 为 this 的 configuration。
-
令 attribute 为用 规范化属性处理 attribute 的结果。
-
如果 configuration["
attributes"] 存在:-
注释:如果有全局允许列表,需要添加 attribute。
-
如果 configuration["
dataAttributes"] 为 true,且 attribute 为 自定义 data 属性,则返回 false。 -
如果 configuration["
attributes"] 包含 attribute 返回 false。 -
注释:修正每个元素的允许/移除列表。
-
如果 configuration["
elements"] 存在:-
对于 configuration["
elements"] 中的每个 element:-
如果 element["
attributes"] 默认为 « » 包含 attribute:-
移除 element["
attributes"] 中的 attribute。
-
-
断言:element["
removeAttributes"] 默认为 « » 不 包含 attribute。
-
-
-
追加 attribute 到 configuration["
attributes"] -
返回 true。
-
-
否则:
-
注释:如果有全局移除列表,需要移除 attribute。
-
如果 configuration["
removeAttributes"] 不 包含 attribute:-
返回 false。
-
-
移除 attribute 从 configuration["
removeAttributes"]。 -
返回 true。
-
setComments(allow) 方法步骤如下:
setDataAttributes(allow) 方法步骤如下:
-
令 configuration 为 this 的 configuration。
-
如果 configuration["
attributes"] 不 存在,则返回 false。 -
如果 configuration["
dataAttributes"] 等于 allow,则返回 false。 -
如果 allow 为 true:
-
移除 configuration["
attributes"] 中所有 attr,其中 attr 为 自定义 data 属性。 -
如果 configuration["
elements"] 存在:-
对于 configuration["
elements"] 中的每个 element:-
如果 element["
attributes"] 存在:-
移除 element["
attributes"] 中所有 attr,其中 attr 为 自定义 data 属性。
-
-
-
-
-
将 configuration["
dataAttributes"] 设为 allow。 -
返回 true。
2.3. 配置字典
dictionary {SanitizerElementNamespace required DOMString ;name DOMString ?= "http://www.w3.org/1999/xhtml"; }; // Used by "elements"_namespace dictionary :SanitizerElementNamespaceWithAttributes SanitizerElementNamespace {sequence <SanitizerAttribute >;attributes sequence <SanitizerAttribute >; };removeAttributes typedef (DOMString or SanitizerElementNamespace );SanitizerElement typedef (DOMString or SanitizerElementNamespaceWithAttributes );SanitizerElementWithAttributes dictionary {SanitizerProcessingInstruction required DOMString ; };target typedef (DOMString or SanitizerProcessingInstruction );SanitizerPI dictionary {SanitizerAttributeNamespace required DOMString ;name DOMString ?=_namespace null ; };typedef (DOMString or SanitizerAttributeNamespace );SanitizerAttribute dictionary {SanitizerConfig sequence <SanitizerElementWithAttributes >;elements sequence <SanitizerElement >;removeElements sequence <SanitizerElement >;replaceWithChildrenElements sequence <SanitizerPI >;processingInstructions sequence <SanitizerPI >;removeProcessingInstructions sequence <SanitizerAttribute >;attributes sequence <SanitizerAttribute >;removeAttributes boolean ;comments boolean ; };dataAttributes
2.4. 配置不变式
配置可以且应该由开发者根据其用途进行修改。可选方式包括从零开始编写新的配置字典,通过修改方法修改已有Sanitizer的配置,或用get()得到已有Sanitizer的配置字典,修改后再创建新Sanitizer实例。
空配置(当用“unsafe”方法如setHTMLUnsafe调用时)允许所有内容。
配置"default"包含内置安全默认配置。
注意“safe”和“unsafe”净化方法有不同的默认值。
并非所有配置字典都是有效的。有效配置应避免冗余(如重复允许同一元素)和矛盾(如同时移除和允许同一元素)。
配置要有效,需满足如下条件:
-
全局允许列表与移除列表混用:
-
elements或removeElements可以存在, 但不能同时存在。 如果两者都缺失,则等价于removeElements设为 « »。 -
attributes或removeAttributes可以存在, 但不能同时存在。 如果两者都缺失,则等价于removeAttributes设为 « »。 -
dataAttributes在概念上是attributes允许列表的扩展。仅在使用attributes列表时,才允许dataAttributes属性。
-
-
不同全局列表之间不允许有重复项:
-
elements、removeElements或replaceWithChildrenElements之间不得有重复项(即不能有相同元素)。 -
attributes和removeAttributes之间不得有重复项(即不能有相同属性)。
-
-
同一元素上的本地允许/移除列表混用:
-
当存在
attributes列表时,该元素上可以有两个、一个或没有attributes和removeAttributes列表。 -
当存在
removeAttributes列表时,该元素上可以有一个或没有attributes和removeAttributes列表,但不能同时有。
-
-
同一元素上的重复项:
-
同一元素上的
attributes和removeAttributes之间不得有重复项。
-
-
内置不可替换元素列表 中的任何元素都不会出现在
replaceWithChildrenElements中,因为用其子元素替换这些元素可能导致重新解析问题或无效的节点树。
elements
元素允许列表还可以为给定元素指定允许或移除的属性。这意在映射到 [HTML] 的结构,其同时包含
全局属性 和适用于特定元素的局部 属性。全局与局部属性可以混用,但请注意,如果某个配置存在歧义——某属性在一处被允许而在另一处被禁止——通常视为无效。
全局 attributes
| 全局 removeAttributes
| |
|---|---|---|
局部 attributes
| 若属性在任一列表中匹配,则允许该属性。不得有重复条目。 | 只有当属性在局部允许列表中时才允许。 全局移除与局部允许列表之间不得有重复条目。 注意:全局移除列表对这个特定元素不起作用,但可能适用于没有局部允许列表的其他元素。 |
局部 removeAttributes
| 若属性在全局允许列表中且不在局部移除列表中,则允许该属性。局部移除必须是全局允许列表的子集。 | 若属性既不在全局移除列表也不在局部移除列表中,则允许该属性。 全局移除与局部移除列表之间不得有重复条目。 |
请注意其中的不对称性:大多数情况下,全局与逐元素列表之间不允许有重复;但当存在全局允许列表与逐元素移除列表时,后者必须是前者的子集。上表中仅聚焦重复条目的摘录如下:
全局 attributes
| 全局 removeAttributes
| |
|---|---|---|
局部 attributes
| 不允许有重复条目。 | 不允许有重复条目。 |
局部 removeAttributes
| 局部移除必须是全局允许列表的子集。 | 不允许有重复条目。 |
dataAttributes
设置允许 自定义数据属性。若将
dataAttributes
视为一个允许列表,则上述规则可直接延伸到 自定义数据属性:
全局 attributes
与 dataAttributes
已设置
| |
|---|---|
局部 attributes
| 所有 自定义数据属性 都被允许。任何 自定义数据属性 都不得被列入任何允许列表,否则将造成重复条目。 |
局部 removeAttributes
| 除非被列入局部移除列表,否则 自定义数据属性 是被允许的。 任何 自定义数据属性 都不得被列入全局允许列表,否则将造成重复条目。 |
用文字表述这些规则:
-
全局与局部列表之间的重复与交互:
-
如果存在全局
attributes允许列表,则所有元素的局部列表:-
若存在局部
attributes允许列表, 则这些列表之间不得有重复条目。 -
若存在局部
removeAttributes移除列表, 则其所有条目也必须出现在全局attributes允许列表中。 -
如果
dataAttributes为 true,则任何 自定义数据属性 都不得被列入任一允许列表。
-
-
如果存在全局
removeAttributes移除列表,则:-
若存在局部
attributes允许列表, 则这些列表之间不得有重复条目。 -
若存在局部
removeAttributes移除列表, 则这些列表之间不得有重复条目。 -
不能同时存在局部
attributes允许列表 与局部removeAttributes移除列表。 -
dataAttributes必须不存在。
-
-
SanitizerConfig
config 是否
有效:
注意:期望传入的配置已事先经过 规范化配置步骤。 这里只会断言该算法应已保证成立的条件。
-
断言:config["
elements"] 存在或 config["removeElements"] 存在。 -
如果 config["
elements"] 存在,且 config["removeElements"] 存在,则返回 false。 -
断言:要么 config["
processingInstructions"] 存在, 要么 config["removeProcessingInstructions"] 存在。 -
如果 config["
processingInstructions"] 存在,且 config["removeProcessingInstructions"] 存在,则返回 false。 -
断言:要么 config["
attributes"] 存在, 要么 config["removeAttributes"] 存在。 -
如果 config["
attributes"] 存在,且 config["removeAttributes"] 存在,则返回 false。 -
断言: config 里的所有
SanitizerElementNamespaceWithAttributes、SanitizerElementNamespace、SanitizerProcessingInstruction、 以及SanitizerAttributeNamespace条目都是规范化的,即它们已经 分别通过 规范化元素、规范化处理指令、或 规范化属性 处理。 -
否则:
-
如果 config["
removeElements"] 有重复项,则返回 false。
-
-
如果 config["
replaceWithChildrenElements"] 存在且 有重复项,则返回 false。 -
如果 config["
processingInstructions"] 存在:-
如果 config["
processingInstructions"] 有重复目标,则返回 false。
-
-
否则:
-
如果 config["
removeProcessingInstructions"] 有重复目标,则返回 false。
-
-
如果 config["
attributes"] 存在:-
如果 config["
attributes"] 有重复项,则返回 false。
-
-
否则:
-
如果 config["
removeAttributes"] 有重复项,则返回 false。
-
-
如果 config["
replaceWithChildrenElements"] 存在:-
对于 config["
replaceWithChildrenElements"] 里的每个 element:-
如果 内建不可替换元素列表 包含 element, 则返回 false。
-
-
-
如果 交集 config["
elements"] 和 config["replaceWithChildrenElements"] 非 空, 则返回 false。
-
-
否则:
-
如果 交集 config["
removeElements"] 和 config["replaceWithChildrenElements"] 非 空, 则返回 false。
-
-
-
如果 config["
attributes"] 存在:-
断言: config["
dataAttributes"] 存在。 -
-
对于 config["
elements"] 里的每个 element:-
如果 element["
attributes"] 存在 且 element["attributes"] 有重复项, 则返回 false。 -
如果 element["
removeAttributes"] 存在,且 element["removeAttributes"] 有重复项, 则返回 false。 -
如果 交集 config["
attributes"] 和 element["attributes"] 默认为 « » 非 空,则返回 false。 -
如果 element["
removeAttributes"] 默认为 « » 不是 config["attributes"] 的子集, 则返回 false。 -
如果 config["
dataAttributes"] 为 true,且 element["attributes"] 含有 自定义 data 属性,则返回 false。
-
-
-
如果 config["
dataAttributes"] 为 true 且 config["attributes"] 含有 自定义 data 属性,则返回 false。
-
-
否则:
-
-
对于 config["
elements"] 里的每个 element:-
如果 element["
attributes"] 存在,且 element["removeAttributes"] 存在, 则返回 false。 -
如果 element["
attributes"] 存在, 且 element["attributes"] 有重复项, 则返回 false。 -
如果 element["
removeAttributes"] 存在, 且 element["removeAttributes"] 有重复项, 则返回 false。 -
如果 交集 config["
removeAttributes"] 和 element["attributes"] 默认为 « » 非 空,则返回 false。 -
如果 交集 config["
removeAttributes"] 和 element["removeAttributes"] 默认为 « » 非 空,则返回 false。
-
-
-
如果 config["
dataAttributes"] 存在,则返回 false。
-
-
返回 true。
注意:
从 字典设置
配置时,会做一些归一化处理。特别是,如果允许/移除列表都缺失,会将其当作空的移除列表。
所以 {} 本身不是 有效 配置,
但会被归一化为 {removeElements:[],removeAttributes:[]},这是有效的。
选用这种规范化,是为了让缺失字典和空字典一致,
即让 setHTMLUnsafe(txt) 和 setHTMLUnsafe(txt, {sanitizer: {}}) 保持一致。
3. 算法
Element
或 DocumentFragment
target,一个 Element
contextElement,一个 字符串 html,一个
字典 options 和一个 布尔值 safe:
-
如果 safe 为真且 contextElement 的 本地名 为 "
script",且 contextElement 的 命名空间为 HTML 命名空间 或 SVG 命名空间,则返回。 -
令 sanitizer 为调用 根据 options 获取 sanitizer 实例 的结果,参数为 options 和 safe。
-
令 newChildren 为根据 HTML 片段解析算法,传入 contextElement、html 和 true 的结果。
-
令 fragment 为一个新的
DocumentFragment, 其 节点文档为 contextElement 的 节点文档。 -
对 fragment 使用 sanitizer 和 safe 执行 sanitize。
-
在 target 内 用 fragment 替换所有。
注意: 该算法适用于 SetHTMLOptions
及
SetHTMLUnsafeOptions,
它们仅在默认值上有所不同。
3.1. 净化
ParentNode
node,一个
Sanitizer
sanitizer,以及一个 布尔值 safe,执行以下步骤:
-
令 configuration 为 sanitizer 的 configuration 的值。
-
如果 safe 为 true,则将 configuration 设为对其调用 remove unsafe 后的结果。
-
对 node、configuration 以及 handleJavascriptNavigationUrls 设为 safe,调用 sanitize core。
ParentNode
node,SanitizerConfig
configuration,以及
布尔值
handleJavascriptNavigationUrls,递归遍历以 node
为起点的 DOM 树。具体步骤如下:
-
对于 node 的 children 里的每个 child:
-
断言:child 实现
Text、Comment、Element、ProcessingInstruction或DocumentType。注意:目前该算法仅对 HTML 解析器输出调用,上述断言应满足。
DocumentType仅会出现在parseHTML和parseHTMLUnsafe。 如果将来该算法用于其它场景,则需重新检查此假设。 -
如果 child 实现
DocumentType, 继续。 -
如果 child 实现
ProcessingInstruction:-
令 piTarget 为 child 的 target。
-
如果 configuration["
processingInstructions"] 存在:-
如果 configuration["
processingInstructions"] 不 包含 piTarget:-
移除 child。
-
-
-
否则:
-
如果 configuration["
removeProcessingInstructions"] 包含 piTarget:-
移除 child。
-
-
-
-
否则:
-
令 elementName 为
SanitizerElementNamespace,值为 child 的 本地名 和 命名空间。 -
如果 configuration["
replaceWithChildrenElements"] 存在, 且 configuration["replaceWithChildrenElements"] 包含 elementName:-
对子 child 以及 configuration 和 handleJavascriptNavigationUrls 调用 sanitize core。
-
调用 replace all, 用 child 的 children 替换 child。
-
继续。
-
否则:
-
如果 configuration["
removeElements"] 包含 elementName:
-
-
如果 elementName 等于 «[ "
name" → "template", "namespace" → HTML namespace ]», 则对子 child 的 template contents 调用 sanitize core,参数为 configuration 与 handleJavascriptNavigationUrls。 -
如果 child 是 shadow host, 对 child 的 shadow root 调用 sanitize core,参数为 configuration 和 handleJavascriptNavigationUrls。
-
令 elementWithLocalAttributes 为 « [] »。
-
如果 configuration["
elements"] 存在 且 configuration["elements"] 包含 elementName:-
将 elementWithLocalAttributes 设为 configuration["
elements"][elementName]。
-
-
对于 child 的 attribute list 里的每个 attribute:
-
令 attrName 为
SanitizerAttributeNamespace,值为 attribute 的 本地名 和 命名空间。 -
如果 elementWithLocalAttributes["
removeAttributes"] 默认为 « » 包含 attrName:-
移除 attribute。
-
-
否则,如果 configuration["
attributes"] 存在:-
如果 configuration["
attributes"] 不 包含 attrName, 且 elementWithLocalAttributes["attributes"] 默认为 « » 不 包含 attrName,并且 "data-" 不是 码元前缀于 attribute 的 本地名 ,且 命名空间 不是null,或者 configuration["dataAttributes"] 不为 true:-
移除 attribute。
-
-
-
否则:
-
如果 elementWithLocalAttributes["
attributes"] 存在 且 elementWithLocalAttributes["attributes"] 不 包含 attrName:-
移除 attribute。
-
-
否则,如果 configuration["
removeAttributes"] 包含 attrName:-
移除 attribute。
-
-
-
如果 handleJavascriptNavigationUrls:
-
如果 «[elementName, attrName]» 匹配 内建导航 URL 属性列表 的一项,且 attribute 包含 javascript: URL,则 移除 attribute。
-
如果 child 的 命名空间 等于 MathML 命名空间,且 attr 的 本地名 等于 "
href" 且 attr 的 命名空间 是null或为 XLink 命名空间 且 attr 包含 javascript: URL, 则 移除 attribute。 -
如果 内建动画 URL 属性列表 包含 «[elementName, attrName]»,且 attr 的 值 等于 "
href" 或 "xlink:href",则 移除 attribute。
-
-
-
对子 child 及 configuration、handleJavascriptNavigationUrls 调用 sanitize core。
-
-
javascript: URL。
由于导航本身不是 XSS 威胁,我们会处理 javascript: URL 的导航,但不会阻断所有导航。
声明式导航大致有几类:
前两类在 内建导航 URL 属性列表覆盖。
MathML 情况用单独规则覆盖,因为本规范没有“每命名空间全局”规则的形式化表示。
SVG 动画情形用
内建动画 URL 属性列表覆盖。但
因为 SVG 动画元素解释依赖动画目标,而在净化时无法得知最终目标,
sanitize 算法会阻断所有关于 href
属性的动画。
-
令 url 为对 attribute 的 值运行基本 URL 解析器的结果。
-
如果 url 是
failure,则返回 false。
3.2. 修改配置
配置修改方法是 Sanitizer
上用于修改其配置的方法。
它们会保持配置的有效性要求。
方法返回一个布尔值,用于告知调用者配置是否发生了更改。
let s= new Sanitizer({ elements: [ "div" ]}); s. allowElement( "p" ); // 返回 true。 div. setHTML( "<div><p>" , { sanitizer: s}); // 允许 `<div>` 和 `<p>`。
let s= new Sanitizer({ elements: [ "div" ]}); s. removeElement( "p" ); // 返回 false,因为 <p> 之前未被允许。 div. setHTML( "<div><p>" , { sanitizer: s}); // 只允许 `<div>`。`<p>` 会被移除。
SanitizerConfig
configuration 中
移除元素 SanitizerElement
element :
-
是使用全局允许列表还是全局移除列表,
-
对应列表是否已经包含 element。
-
令 element 为用 规范化元素处理 element 的结果。
-
令 modified 为 remove element 从 configuration["
replaceWithChildrenElements"] 的结果。 -
否则:
-
如果 configuration["
removeElements"] 包含 element:-
注释:我们有全局移除列表,且其中已含 element。
-
返回 modified。
-
-
注释:我们有全局移除列表,但其中不含 element。
-
添加 element 到 configuration["
removeElements"]。 -
返回 true。
-
SanitizerConfig
configuration 中
移除属性 SanitizerAttribute attribute :
注意:此方法区分两种情形,即全局允许列表还是全局移除列表。如果向全局移除列表添加 attribute, 可能需要额外处理每个元素的允许或移除列表,以保证有效性要求。如果从全局允许列表移除 attribute,也可能要从本地移除列表一并删去该属性。
-
令 attribute 为用 规范化属性处理 attribute 的结果。
-
如果 configuration["
attributes"] 存在:-
注释:如果有全局允许列表,需要移除 attribute。
-
令 modified 为 remove attribute 从 configuration["
attributes"] 的结果。 -
注释:修正各元素特定的允许/移除列表。
-
如果 configuration["
elements"] 存在:-
对于 configuration["
elements"] 里的每个 element :-
如果 element["
attributes"] 默认为 « » 包含 attribute:-
令 modified = true。
-
移除 attribute 从 element["
attributes"]。
-
-
如果 element["
removeAttributes"] 默认为 « » 包含 attribute:-
断言:modified 为 true。
-
移除 attribute 从 element["
removeAttributes"]。
-
-
-
-
返回 modified。
-
-
否则:
-
注释:如果有全局移除列表,需要添加 attribute。
-
如果 configuration["
removeAttributes"] 包含 attribute 返回 false。 -
注释:修正各元素特定的允许/移除列表。
-
如果 configuration["
elements"] 存在:-
对于 configuration["
elements"] 中的每个 element:-
如果 element["
attributes"] 默认为 « » 包含 attribute:-
移除 attribute 从 element["
attributes"]。
-
-
如果 element["
removeAttributes"] 默认为 « » 包含 attribute:-
移除 attribute 从 element["
removeAttributes"]。
-
-
-
-
添加 attribute 到 configuration["
removeAttributes"]。 -
返回 true。
-
SanitizerConfig
configuration 中
移除不安全内容,应执行:
注意:虽然本算法被命名为 remove unsafe, 实际上我们仅在本规范意义下 用“unsafe”指代 插入文档后会执行 JavaScript 的内容。换言之, 此方法会消除 XSS 攻击的机会。
-
断言:内置安全基线配置的键集合 等于 « [ "
removeElements", "removeAttributes" ] »。 -
令 result 为 false。
-
对于 内建安全基线配置["
removeElements"] 里的每个 element:-
调用 移除元素,参数为 element 和 configuration。
-
如果调用结果为 true,则将 result 设为 true。
-
-
对于 内建安全基线配置["
removeAttributes"] 里的每个 attribute:-
调用 移除属性,参数为 attribute 和 configuration。
-
如果调用结果为 true,则将 result 设为 true。
-
-
-
调用 移除属性,参数为 attribute 和 configuration。
-
如果调用结果为 true,则将 result 设为 true。
-
-
返回 result。
3.3. 设置配置
Sanitizer
sanitizer:
-
对 configuration 使用 allowCommentsPIsAndDataAttributes 执行 规范化。
-
如果 configuration 不是 有效,则返回 false。
-
将 sanitizer 的 configuration 设置为 configuration。
-
返回 true。
3.4. 规范化配置
Sanitizer
以规范化形式存储配置,这样会让多种处理步骤变得更简单。
elements
列表 {elements: ["div"]} 存储时会变成
{elements: [{name: "div", namespace: "http://www.w3.org/1999/xhtml"}]。
SanitizerConfig
configuration
和一个布尔值
allowCommentsPIsAndDataAttributes:
注意:假定 configuration 已由 [WebIDL]
将 JavaScript 值转换为 SanitizerConfig
的结果。
-
如果 configuration["
elements"] 和 configuration["removeElements"] 均不存在,则设定 configuration["removeElements"] 为 « »。 -
如果 configuration["
processingInstructions"] 和 configuration["removeProcessingInstructions"] 均不存在:-
如果 allowCommentsPIsAndDataAttributes 为 true,设定 configuration["
removeProcessingInstructions"] 为 « »。 -
否则,设定 configuration["
processingInstructions"] 为 « »。
-
-
如果 configuration["
attributes"] 和 configuration["removeAttributes"] 均不存在,则设定 configuration["removeAttributes"] 为 « »。 -
如果 configuration["
elements"] 存在:-
令 elements = « »。
-
对于 configuration["
elements"] 里的每个 element:-
将 规范化带属性的净化元素 element 的结果添加到 elements。
-
-
设置 configuration["
elements"] 为 elements。
-
-
如果 configuration["
removeElements"] 存在:-
令 elements = « »。
-
对于 configuration["
removeElements"] 里的每个 element: -
设置 configuration["
removeElements"] 为 elements。
-
-
如果 configuration["
replaceWithChildrenElements"] 存在:-
令 elements = « »。
-
对于 configuration["
replaceWithChildrenElements"] 里的每个 element: -
设置 configuration["
replaceWithChildrenElements"] 为 elements。
-
-
如果 configuration["
processingInstructions"] 存在:-
令 processingInstructions = « »。
-
对于 configuration["
processingInstructions"] 中的每个 pi: -
设置 configuration["
processingInstructions"] 为 processingInstructions。
-
-
如果 configuration["
removeProcessingInstructions"] 存在:-
令 processingInstructions = « »。
-
对于 configuration["
removeProcessingInstructions"] 的每个 pi: -
设置 configuration["
removeProcessingInstructions"] 为 processingInstructions。
-
-
如果 configuration["
attributes"] 存在:-
令 attributes = « »。
-
对于 configuration["
attributes"] 的每个 attribute: -
设置 configuration["
attributes"] 为 attributes。
-
-
如果 configuration["
removeAttributes"] 存在:-
令 attributes = « »。
-
对于 configuration["
removeAttributes"] 的每个 attribute: -
设置 configuration["
removeAttributes"] 为 attributes。
-
-
如果 configuration["
comments"] 不存在,则设定 configuration["comments"] 为 allowCommentsPIsAndDataAttributes。 -
如果 configuration["
attributes"] 存在 且 configuration["dataAttributes"] 不存在,则 设置 configuration["dataAttributes"] 为 allowCommentsPIsAndDataAttributes。
SanitizerElementWithAttributes
element:
-
令 result 为用 规范化元素处理 element 的结果。
-
如果 element 是字典:
-
如果 element["
attributes"] 存在:-
令 attributes = « »。
-
对于 element["
attributes"] 的每个 attribute: -
设置 result["
attributes"] 为 attributes。
-
-
如果 element["
removeAttributes"] 存在:-
令 attributes = « »。
-
对于 element["
removeAttributes"] 的每个 attribute: -
设置 result["
removeAttributes"] 为 attributes。
-
-
-
如果 result["
attributes"] 和 result["removeAttributes"] 均不存在:-
设置 result["
removeAttributes"] 为 « »。
-
-
返回 result。
SanitizerAttribute
attribute,
返回用 attribute 和 null 作为默认命名空间,调用 规范化名称 的结果。
3.5. 辅助算法
对于本规范中使用的 规范化
元素
和 属性名
列表,成员资格基于同时匹配 "name" 和
"namespace"
两个条目:
SanitizerElement
的列表 A 和
B 的
交集
与 集合交集 相同,
但集合中的条目需先经过
规范化:
-
令 set A 为 « [] »。
-
令 set B 为 « [] »。
-
对于 A 中的每个 entry,将 规范化 sanitizer 名称的结果 entry 添加到 set A。
-
对于 B 中的每个 entry,将 规范化 sanitizer 名称的结果 entry 添加到 set B。
-
返回 set A 和 set B 的 交集。
3.6. 内置项
有五个内置项:
内置安全默认配置 如下:
{ "elements" : [ { "name" : "math" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "merror" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mfrac" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mi" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mmultiscripts" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mn" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mo" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "fence" , "namespace" : null }, { "name" : "form" , "namespace" : null }, { "name" : "largeop" , "namespace" : null }, { "name" : "lspace" , "namespace" : null }, { "name" : "maxsize" , "namespace" : null }, { "name" : "minsize" , "namespace" : null }, { "name" : "movablelimits" , "namespace" : null }, { "name" : "rspace" , "namespace" : null }, { "name" : "separator" , "namespace" : null }, { "name" : "stretchy" , "namespace" : null }, { "name" : "symmetric" , "namespace" : null } ] }, { "name" : "mover" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "accent" , "namespace" : null } ] }, { "name" : "mpadded" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "depth" , "namespace" : null }, { "name" : "height" , "namespace" : null }, { "name" : "lspace" , "namespace" : null }, { "name" : "voffset" , "namespace" : null }, { "name" : "width" , "namespace" : null } ] }, { "name" : "mphantom" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mprescripts" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mroot" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mrow" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "ms" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mspace" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "depth" , "namespace" : null }, { "name" : "height" , "namespace" : null }, { "name" : "width" , "namespace" : null } ] }, { "name" : "msqrt" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mstyle" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "msub" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "msubsup" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "msup" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mtable" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mtd" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "columnspan" , "namespace" : null }, { "name" : "rowspan" , "namespace" : null } ] }, { "name" : "mtext" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "mtr" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "munder" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "accentunder" , "namespace" : null } ] }, { "name" : "munderover" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [ { "name" : "accent" , "namespace" : null }, { "name" : "accentunder" , "namespace" : null } ] }, { "name" : "semantics" , "namespace" : "http://www.w3.org/1998/Math/MathML" , "attributes" : [] }, { "name" : "a" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "href" , "namespace" : null }, { "name" : "hreflang" , "namespace" : null }, { "name" : "type" , "namespace" : null } ] }, { "name" : "abbr" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "address" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "article" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "aside" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "b" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "bdi" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "bdo" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "blockquote" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "cite" , "namespace" : null } ] }, { "name" : "body" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "br" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "caption" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "cite" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "code" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "col" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "span" , "namespace" : null } ] }, { "name" : "colgroup" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "span" , "namespace" : null } ] }, { "name" : "data" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "value" , "namespace" : null } ] }, { "name" : "dd" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "del" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "cite" , "namespace" : null }, { "name" : "datetime" , "namespace" : null } ] }, { "name" : "dfn" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "div" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "dl" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "dt" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "em" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "figcaption" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "figure" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "footer" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "h1" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "h2" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "h3" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "h4" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "h5" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "h6" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "head" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "header" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "hgroup" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "hr" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "html" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "i" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "ins" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "cite" , "namespace" : null }, { "name" : "datetime" , "namespace" : null } ] }, { "name" : "kbd" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "li" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "value" , "namespace" : null } ] }, { "name" : "main" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "mark" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "menu" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "nav" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "ol" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "reversed" , "namespace" : null }, { "name" : "start" , "namespace" : null }, { "name" : "type" , "namespace" : null } ] }, { "name" : "p" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "pre" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "q" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "rp" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "rt" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "ruby" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "s" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "samp" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "search" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "section" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "small" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "span" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "strong" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "sub" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "sup" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "table" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "tbody" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "td" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "colspan" , "namespace" : null }, { "name" : "headers" , "namespace" : null }, { "name" : "rowspan" , "namespace" : null } ] }, { "name" : "tfoot" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "th" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "abbr" , "namespace" : null }, { "name" : "colspan" , "namespace" : null }, { "name" : "headers" , "namespace" : null }, { "name" : "rowspan" , "namespace" : null }, { "name" : "scope" , "namespace" : null } ] }, { "name" : "thead" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "time" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [ { "name" : "datetime" , "namespace" : null } ] }, { "name" : "title" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "tr" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "u" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "ul" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "var" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "wbr" , "namespace" : "http://www.w3.org/1999/xhtml" , "attributes" : [] }, { "name" : "a" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "href" , "namespace" : null }, { "name" : "hreflang" , "namespace" : null }, { "name" : "type" , "namespace" : null } ] }, { "name" : "circle" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "cx" , "namespace" : null }, { "name" : "cy" , "namespace" : null }, { "name" : "pathLength" , "namespace" : null }, { "name" : "r" , "namespace" : null } ] }, { "name" : "defs" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [] }, { "name" : "desc" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [] }, { "name" : "ellipse" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "cx" , "namespace" : null }, { "name" : "cy" , "namespace" : null }, { "name" : "pathLength" , "namespace" : null }, { "name" : "rx" , "namespace" : null }, { "name" : "ry" , "namespace" : null } ] }, { "name" : "foreignObject" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "height" , "namespace" : null }, { "name" : "width" , "namespace" : null }, { "name" : "x" , "namespace" : null }, { "name" : "y" , "namespace" : null } ] }, { "name" : "g" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [] }, { "name" : "line" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "pathLength" , "namespace" : null }, { "name" : "x1" , "namespace" : null }, { "name" : "x2" , "namespace" : null }, { "name" : "y1" , "namespace" : null }, { "name" : "y2" , "namespace" : null } ] }, { "name" : "marker" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "markerHeight" , "namespace" : null }, { "name" : "markerUnits" , "namespace" : null }, { "name" : "markerWidth" , "namespace" : null }, { "name" : "orient" , "namespace" : null }, { "name" : "preserveAspectRatio" , "namespace" : null }, { "name" : "refX" , "namespace" : null }, { "name" : "refY" , "namespace" : null }, { "name" : "viewBox" , "namespace" : null } ] }, { "name" : "metadata" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [] }, { "name" : "path" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "d" , "namespace" : null }, { "name" : "pathLength" , "namespace" : null } ] }, { "name" : "polygon" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "pathLength" , "namespace" : null }, { "name" : "points" , "namespace" : null } ] }, { "name" : "polyline" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "pathLength" , "namespace" : null }, { "name" : "points" , "namespace" : null } ] }, { "name" : "rect" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "height" , "namespace" : null }, { "name" : "pathLength" , "namespace" : null }, { "name" : "rx" , "namespace" : null }, { "name" : "ry" , "namespace" : null }, { "name" : "width" , "namespace" : null }, { "name" : "x" , "namespace" : null }, { "name" : "y" , "namespace" : null } ] }, { "name" : "svg" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "height" , "namespace" : null }, { "name" : "preserveAspectRatio" , "namespace" : null }, { "name" : "viewBox" , "namespace" : null }, { "name" : "width" , "namespace" : null }, { "name" : "x" , "namespace" : null }, { "name" : "y" , "namespace" : null } ] }, { "name" : "text" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "dx" , "namespace" : null }, { "name" : "dy" , "namespace" : null }, { "name" : "lengthAdjust" , "namespace" : null }, { "name" : "rotate" , "namespace" : null }, { "name" : "textLength" , "namespace" : null }, { "name" : "x" , "namespace" : null }, { "name" : "y" , "namespace" : null } ] }, { "name" : "textPath" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "lengthAdjust" , "namespace" : null }, { "name" : "method" , "namespace" : null }, { "name" : "path" , "namespace" : null }, { "name" : "side" , "namespace" : null }, { "name" : "spacing" , "namespace" : null }, { "name" : "startOffset" , "namespace" : null }, { "name" : "textLength" , "namespace" : null } ] }, { "name" : "title" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [] }, { "name" : "tspan" , "namespace" : "http://www.w3.org/2000/svg" , "attributes" : [ { "name" : "dx" , "namespace" : null }, { "name" : "dy" , "namespace" : null }, { "name" : "lengthAdjust" , "namespace" : null }, { "name" : "rotate" , "namespace" : null }, { "name" : "textLength" , "namespace" : null }, { "name" : "x" , "namespace" : null }, { "name" : "y" , "namespace" : null } ] } ], "processingInstructions" : [], "attributes" : [ { "name" : "alignment-baseline" , "namespace" : null }, { "name" : "baseline-shift" , "namespace" : null }, { "name" : "clip-path" , "namespace" : null }, { "name" : "clip-rule" , "namespace" : null }, { "name" : "color" , "namespace" : null }, { "name" : "color-interpolation" , "namespace" : null }, { "name" : "cursor" , "namespace" : null }, { "name" : "dir" , "namespace" : null }, { "name" : "direction" , "namespace" : null }, { "name" : "display" , "namespace" : null }, { "name" : "displaystyle" , "namespace" : null }, { "name" : "dominant-baseline" , "namespace" : null }, { "name" : "fill" , "namespace" : null }, { "name" : "fill-opacity" , "namespace" : null }, { "name" : "fill-rule" , "namespace" : null }, { "name" : "font-family" , "namespace" : null }, { "name" : "font-size" , "namespace" : null }, { "name" : "font-size-adjust" , "namespace" : null }, { "name" : "font-stretch" , "namespace" : null }, { "name" : "font-style" , "namespace" : null }, { "name" : "font-variant" , "namespace" : null }, { "name" : "font-weight" , "namespace" : null }, { "name" : "lang" , "namespace" : null }, { "name" : "letter-spacing" , "namespace" : null }, { "name" : "marker-end" , "namespace" : null }, { "name" : "marker-mid" , "namespace" : null }, { "name" : "marker-start" , "namespace" : null }, { "name" : "mathbackground" , "namespace" : null }, { "name" : "mathcolor" , "namespace" : null }, { "name" : "mathsize" , "namespace" : null }, { "name" : "opacity" , "namespace" : null }, { "name" : "paint-order" , "namespace" : null }, { "name" : "pointer-events" , "namespace" : null }, { "name" : "scriptlevel" , "namespace" : null }, { "name" : "shape-rendering" , "namespace" : null }, { "name" : "stop-color" , "namespace" : null }, { "name" : "stop-opacity" , "namespace" : null }, { "name" : "stroke" , "namespace" : null }, { "name" : "stroke-dasharray" , "namespace" : null }, { "name" : "stroke-dashoffset" , "namespace" : null }, { "name" : "stroke-linecap" , "namespace" : null }, { "name" : "stroke-linejoin" , "namespace" : null }, { "name" : "stroke-miterlimit" , "namespace" : null }, { "name" : "stroke-opacity" , "namespace" : null }, { "name" : "stroke-width" , "namespace" : null }, { "name" : "text-anchor" , "namespace" : null }, { "name" : "text-decoration" , "namespace" : null }, { "name" : "text-overflow" , "namespace" : null }, { "name" : "text-rendering" , "namespace" : null }, { "name" : "title" , "namespace" : null }, { "name" : "transform" , "namespace" : null }, { "name" : "transform-origin" , "namespace" : null }, { "name" : "unicode-bidi" , "namespace" : null }, { "name" : "vector-effect" , "namespace" : null }, { "name" : "visibility" , "namespace" : null }, { "name" : "white-space" , "namespace" : null }, { "name" : "word-spacing" , "namespace" : null }, { "name" : "writing-mode" , "namespace" : null } ], "comments" : false , "dataAttributes" : false }
注意:所包含的 [MathML] 标记基于 [SafeMathML]。
内置安全基线配置旨在仅阻止脚本内容。具体如下:
{ "removeElements" : [ { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "embed" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "frame" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "iframe" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "object" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "script" }, { "namespace" : "http://www.w3.org/2000/svg" , "name" : "script" }, { "namespace" : "http://www.w3.org/2000/svg" , "name" : "use" } ], "removeAttributes" : [] }
警告: remove unsafe 算法还规定要额外移除所有 事件处理内容属性,详见 [HTML]。 如果某个 用户代理 在 [HTML] 规范基础上扩展了额外的 事件处理内容属性,则如何处理这些属性由其自行决定。基于当前的 事件处理内容属性 列表, 安全基线配置实际上大致如下所示:
{ "removeElements" : [ { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "embed" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "frame" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "iframe" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "object" }, { "namespace" : "http://www.w3.org/1999/xhtml" , "name" : "script" }, { "namespace" : "http://www.w3.org/2000/svg" , "name" : "script" }, { "namespace" : "http://www.w3.org/2000/svg" , "name" : "use" } ], "removeAttributes" : [ "onafterprint" , "onauxclick" , "onbeforeinput" , "onbeforematch" , "onbeforeprint" , "onbeforeunload" , "onbeforetoggle" , "onblur" , "oncancel" , "oncanplay" , "oncanplaythrough" , "onchange" , "onclick" , "onclose" , "oncontextlost" , "oncontextmenu" , "oncontextrestored" , "oncopy" , "oncuechange" , "oncut" , "ondblclick" , "ondrag" , "ondragend" , "ondragenter" , "ondragleave" , "ondragover" , "ondragstart" , "ondrop" , "ondurationchange" , "onemptied" , "onended" , "onerror" , "onfocus" , "onformdata" , "onhashchange" , "oninput" , "oninvalid" , "onkeydown" , "onkeypress" , "onkeyup" , "onlanguagechange" , "onload" , "onloadeddata" , "onloadedmetadata" , "onloadstart" , "onmessage" , "onmessageerror" , "onmousedown" , "onmouseenter" , "onmouseleave" , "onmousemove" , "onmouseout" , "onmouseover" , "onmouseup" , "onoffline" , "ononline" , "onpagehide" , "onpagereveal" , "onpageshow" , "onpageswap" , "onpaste" , "onpause" , "onplay" , "onplaying" , "onpopstate" , "onprogress" , "onratechange" , "onreset" , "onresize" , "onrejectionhandled" , "onscroll" , "onscrollend" , "onsecuritypolicyviolation" , "onseeked" , "onseeking" , "onselect" , "onslotchange" , "onstalled" , "onstorage" , "onsubmit" , "onsuspend" , "ontimeupdate" , "ontoggle" , "onunhandledrejection" , "onunload" , "onvolumechange" , "onwaiting" , "onwheel" ] }
javascript:" 导航被视为“不安全”,内容如下:
«[
[
{ "name" → "a", "namespace" → HTML 命名空间
},
{ "name" → "href", "namespace" → null }
],
[
{ "name" → "area", "namespace" → HTML 命名空间
},
{ "name" → "href", "namespace" → null }
],
[
{ "name" → "base", "namespace" → HTML 命名空间
},
{ "name" → "href", "namespace" → null }
],
[
{ "name" → "button", "namespace" → HTML 命名空间
},
{ "name" → "formaction", "namespace" → null }
],
[
{ "name" → "form", "namespace" → HTML 命名空间
},
{ "name" → "action", "namespace" → null }
],
[
{ "name" → "input", "namespace" → HTML 命名空间
},
{ "name" → "formaction", "namespace" → null }
],
[
{ "name" → "a", "namespace" → SVG 命名空间 },
{ "name" → "href", "namespace" → null }
],
[
{ "name" → "a", "namespace" → SVG 命名空间 },
{ "name" → "href", "namespace" → XLink 命名空间 }
],
]»
内置动画型
URL 属性列表,可用于
[SVG11] 规范中,通过声明式方式修改导航元素以使用
"javascript:"
URL,其内容如下:
«[
[
{ "name" → "animate", "namespace" → SVG 命名空间 },
{ "name" → "attributeName", "namespace" → null] }
],
[
{ "name" → "animateTransform", "namespace" → SVG 命名空间 },
{ "name" → "attributeName", "namespace" → null }
],
[
{ "name" → "set", "namespace" → SVG 命名空间 },
{ "name" → "attributeName", "namespace" → null }
],
]»
内置不可替换元素列表包含一些元素,这些元素不能用它们的子元素替换,否则可能导致解析问题或无效的节点树。
«[
{ "name" → "html", "namespace" → HTML 命名空间 },
{ "name" → "svg", "namespace" → SVG 命名空间 },
{ "name" → "math", "namespace" → MathML
命名空间 },
]»
这些限制不足以防止所有类型的重新解析攻击,例如:
// 错误示例 let sanitizer= { replaceWithChildrenElements: [ "tr" ] }; div. setHTML( "<table><tbody><tr><td><div>" , { sanitizer}); div. innerHTML= div. innerHTML; // div.innerHTML 现在是 `<div></div><table><tbody><tr></tr></tbody></table>`
不过,这样可以防止本例中的 <iframe> 被重新解析并变成普通 HTML iframe:
4. 安全注意事项
Sanitizer API 旨在通过遍历所提供的 HTML 内容,并根据配置移除元素和属性,防止基于 DOM 的跨站脚本(XSS)攻击。规范要求,API 不应支持构造会遗留可执行脚本标记的 Sanitizer 对象,否则将会破坏威胁模型。
但即便如此,正确使用 Sanitizer API 也无法防护所有安全问题,相关场景会在下文中说明。
4.1. 服务端反射型和存储型 XSS
本节为非规范性内容。
Sanitizer API 仅在 DOM 中操作,提供遍历和过滤现有 DocumentFragment 的能力。Sanitizer 不涉及服务端反射型或存储型 XSS 问题。
4.2. DOM 覆写攻击(clobbering)
本节为非规范性内容。
DOM 覆写攻击指通过恶意 HTML 使用 id 或 name 属性命名元素,从而让 HTML 元素的某些属性(如
children)被恶意内容覆盖,导致应用混淆。
Sanitizer API 的默认行为无法防护 DOM 覆写攻击,但可以通过配置移除 id 和 name 属性。
4.3. 利用脚本组件的 XSS
本节为非规范性内容。
脚本组件攻击是一种攻击者利用流行 JavaScript 库中的已有应用代码,通过注入看似无害的代码或仅被框架解析的 DOM 节点,最终由框架解析并执行这些输入中的 JavaScript 的技术。
Sanitizer API 无法防止此类攻击,要求页面作者一般要显式允许未知元素,并且还需针对未知属性、元素及如 data-、slot 属性和
<slot>、<template> 等广泛用于模板和框架的标记作出明确配置。
我们认为这些限制并不详尽,鼓励页面作者检查所用第三方库是否有此类行为。
4.4. 变异型 XSS(mXSS)
本节为非规范性内容。
变异型 XSS(mXSS)描述的是由于解析器上下文不一致,在错误上下文下解析 HTML 片段时发生攻击。当某个已解析的 HTML 片段序列化为字符串后,在插入到不同父元素时未必能保证解析效果一致。例如,依赖于外部内容或标签嵌套错误导致解析行为变化,即可发动此类攻击。
Sanitizer API 只提供将字符串转为节点树的功能。上下文由所有 sanitizer 函数隐式提供:Element.setHTML()
使用当前元素;Document.parseHTML() 创建新文档。因此 Sanitizer API 不会直接受到变异型 XSS 影响。
如果开发者将净化后的节点树通过 .innerHTML 取字符串,再重新解析,则有可能发生变异型 XSS。我们不推荐这种做法。如果确实需要处理或传递 HTML
字符串,则应将其视为不可信内容,并在插入 DOM 时重新净化。换言之,净化并序列化后的 HTML 树不应再被视作已净化。
关于 mXSS 的更完整讨论可见 [MXSS]。
5. 致谢
本项工作借鉴了 cure53 团队的 [DOMPURIFY],
以及 Internet Explorer 的 window.toStaticHTML()
和 Ben Bucksch 的
[HTMLSanitizer]。感谢 Anne
van Kesteren、Krzysztof Kotowicz、
Andrew C. H. Mc Millan、Tom Schuster、Luke Warlow、Guillaume Weghsteen
以及 Mike West 的宝贵反馈。