![cb33fe1537ea3ed86d007e91ebdb9c55.png](https://i-blog.csdnimg.cn/blog_migrate/df637467df26967470e1f1481bc80fbc.png)
<form><math><mtext>form><form><mglyph><style>math><img src onerror=alert(1)>
以上代码没有任何多余的元素,为了理解具体的绕过原理,我们需要先了解HTML规范的一些特性。
2DOMPurify用法
我们首先了解下DOMPurify通常是如何使用的。假设我们需要将html Markup中一个不受信任的HTML赋值给某个div,我们可以通过以下代码使用DOMPurify对该内容进行过滤,在赋值给div:
div.innerHTML = DOMPurify.sanitize(htmlMarkup)
在解析、系列化以及处理DOM树的过程中,上诉代码实际上会执行以下操作:
htmlMarkup被解析为DOM树
DOMPurify过滤DOM树(简而言之,这个过程中DOMPurify会遍历DOM树中的所有元素以及属性,并删除不在允许列表中的所有节点)
DOM树被序列化回HTML标记
赋值给innerHTML之后,浏览器将再次解析HTML标记
解析的DOM树被附加到文档的DOM树中
![a616be598d793eebb5955e1ab32d2a34.png](https://i-blog.csdnimg.cn/blog_migrate/a5184515aabd9c9274f4da1cd89a7f62.png)
![11e23c4e4a87ef1223d3365985c7e676.png](https://i-blog.csdnimg.cn/blog_migrate/122143476828b1cb0e76330c4e7d6906.png)
A<img src="1">B
这就是DOMPurify.sanitize返回的结果。然后浏览器在赋值给innerHTML时会再次解析该标记。
DOM树与DOMPurify处理过的树相同,然后将其附加到目标文档中。
简而言之,按顺序,以上过程可以总结成:解析->序列化->解析。直观上,大家可能会觉得系列化DOM树,并再次解析后,应该会返回初始的DOM树,但事实并非如此。
HTML规范中,关于系列化HTML片段的警告信息表明:如果使用HTML解析器进行解析,则此算法[序列化HTML]的输出可能不会返回初始的树结构。HTML解析器本身也有可能输出不往返序列化和重新解析步骤的树结构,尽管这种情况通常不符合要求。
值得注意的是,序列号-重新解析的往返操作并不一定能返回初始的DOM树,这也是造成mutation XSS(突变XSS) 的根本原因。通常这类情况是由于某些解析器、序列化器错误所导致的结果,但有两种特殊情况符合上诉警告信息所描述的场景。
3嵌套FORM元素其中一种情况涉及FORM元素,它是HTML里非常特殊的一个元素,因为它本身不能嵌套。HTML规范中,明确指出FORM元素不可以是某个FORM元素的后继。
可以在任何浏览器中使用以下标记进行验证:
<form id=form1>INSIDE_FORM1<form id=form2>INSIDE_FORM2
该片段会生成以下DOM树:
![0769cdd9d7857cb1e6588e7dc37b0e3b.png](https://i-blog.csdnimg.cn/blog_migrate/6be308c1c0a18577753bd72d3bc2a9e6.png)
第2个form在DOM树中会被完全忽略,就像从未存在过一样。
接下来是比较有趣的部分。如果我们继续阅读HTML规范,会发现它实际上给出了一个示例,该示例通过带有错误嵌套标签的标记成功创建了一个嵌套式表单。如下所示:
<form id="outer"><div>form><form id="inner"><input>
结果会生成以下包含一个嵌套表单元素的DOM树:
![106005670ae9385aa364a36a4e1defd6.png](https://i-blog.csdnimg.cn/blog_migrate/88845d09ea7ba9d7d01464e8de9e3049.png)
这并不是特定浏览器的bug,它直接来自HTML规范,并且在解析HTML的算法中也有描述。大意如下所示:
当我们打开标签时,解析器需要记录该标签是由表单元素指针打开的。如果指针不为空,则无法创建表单元素。
当我们结束标签时,表单元素指针始终设置为null。
因此,回到以下片段:
<form id="outer"><div>form><form id="inner"><input>
首先,表单元素指针设置为id =“outer”,然后开始解析div,碰到闭合标签后,表单元素指针会被设置为null。由于指针为null,因此可以创建下一个id=“ inner”的表单。并且由于我们当前位于div内,因此可以成功创建一个嵌套式表单。
现在,如果我们尝试序列化生成的DOM树,将获得以下标记:
<form id="outer"><div><form id="inner"><input>form>div>form>
值得注意的是,该标记将不包含任何错误嵌套的标签。当再次解析标记时,将创建以下DOM树:
![ffee7591b1fae766daabe521a90fdff4.png](https://i-blog.csdnimg.cn/blog_migrate/9a9e997212ff6e73ac788c2a6d0e4b72.png)
综上,我们证明了经过序列化、重解析后并不一定能返回原始的DOM树。更有趣的是,这基本上是一种符合规范的突变。
当我发现这一点后,便意识到可以通过某种方式滥用这种特性以绕过HTML过滤器。思考了很久后,偶然间发现HTML规范中的另一个异常点。在继续讨论该问题前,我们先来了解下HTML规范中的潘多拉魔盒:外部内容(foreign content)。
4外部内容外部内容就好比一把瑞士军刀,可以用来破坏解析器及过滤器。该方法曾被我用来绕过DOMPurify以及Ruby过滤库。
HTML解析器可以使用三个命名空间元素创建DOM树:
HTML命名空间(http://www.w3.org/1999/xhtml)
SVG命名空间(http://www.w3.org/2000/svg)
MathML命名空间(http://www.w3.org/1998/Math/MathML)
默认情况下,所有元素都位于HTML命名空间中。但是,如果解析器遇到或元素,则它将分别“切换”到SVG和MathML命名空间。这两个命名空间都会产生外部内容。
在外部内容中,标记的解析过程与普通的HTML不同。尤其是在解析
思考以下标记:
<style><a>ABCstyle><svg><style><a>ABC
它将被解析成以下DOM树:
![loading.gif](https://i-blog.csdnimg.cn/blog_migrate/0a0b243c837b0e75e9dd332ba399c771.gif)
备注:从现在开始,本文DOM树中的所有元素都将包含一个命名空间。因此html style意味着它是HTML命名空间中的
最终的DOM树证实我的观点:html style仅包含文本内容,而svg style会像普通元素一样被解析。
继续分析,我们可能会猜想:如果我们位于或内部,则所有元素也都会位于非HTML命名空间中。但事实并非如此,HTML规范中包含MathML文本集成点和HTML集成点,这些元素的子元素都具有HTML命名空间(但某些情况除外)。
思考以下示例:
<math>
<style>style>
<mtext><style>style>
它将被解析成以下DOM树
![loading.gif](https://i-blog.csdnimg.cn/blog_migrate/0a0b243c837b0e75e9dd332ba399c771.gif)
请注意观察,在MathML命名空间中,style元素是如何成为math的直接子元素的,而mtext中的style元素则位于HTML命名空间中。这是因为mtext是MathML文本集成点,并使用解析器切换命名空间。
MathML文本集成点包括:
math mi
math mo
math mn
math ms
HTML集成点包括:
math annotation-xml(如果其具有encoding属性,并且属性值等于text/html或者application/xhtml+xml)
svg foreignObject
svg desc
svg title
我之前一直以为MathML文本集成点或HTML集成点的所有子级,默认情况下都具有HTML命名空间,但事实并非如此。HTML规范指出,默认情况下,MathML文本集成点的子级都位于HTML命名空间中。但有两种例外情况:mglyph和malignmark。只有当它们是MathML文本集成点的直接子级时,才会发生这种情况。
我们可以通过以下标记进行检查:
![loading.gif](https://i-blog.csdnimg.cn/blog_migrate/0a0b243c837b0e75e9dd332ba399c771.gif)
注意,作为mtext的直接子元素的mglyph在MathML命名空间中,而作为html元素的子元素的mglyph在HTML命名空间中。
假设我们想确定一个“当前元素”的命名空间。此时我已经制定了一些经验法则:
除非满足以下条件,否则当前元素将位于其父元素的命名空间中。
如果当前元素为或,且其父元素在HTML命名空间中,则当前元素分别位于SVG或MathML命名空间中。
如果当前元素的父元素是HTML集成点,则当前元素位于HTML命名空间中,除非它是或。
如果当前元素的父元素是MathML集成点,则当前元素位于HTML命名空间中,除非它是,,或。
如果当前元素是,,
,,
,, ,,
,
,, ,,
,
,
,
,
,
,,,,,
,,
,,,
,
,
, ,,,或,并定义了color,face或size属性,那么堆栈上的所有元素将关闭,直到看到MathML文本集成点、HTML集成点或HTML命名空间中的元素为止。然后,当前元素也会位于HTML命名空间中。
当我在HTML规范中找到mglyph时,马上就意识到这是滥用html form突变以绕过过滤器的方法。
5
DOMPurify绕过
因此,让我们回到绕过DOMPurify的有效载荷上:
有效载荷利用了错误嵌套的html form元素,并且包含mglyph元素,它将生成以下DOM树:
该DOM树是无害的,所有元素都在DOMPurify的允许列表中。值得注意的是,mglyph位于HTML命名空间中,而看起来像XSS有效载荷的代码片段只是html style中的一个文本。由于存在一个嵌套的html form,所以我们可以确定此DOM树将在重新解析时发生突变。
因此,这里DOMPurify不会执行任何操作,将返回序列化的HTML:
该片段具有嵌套的Form标签。因此,当其赋值给innerHTML时,将被解析成以下DOM树:
所以现在没有创建第二个html表单,mglyph为mtext的直接子元素,这意味着它位于MathML命名空间中。因此,style同样位于MathML命名空间中,且其内容不会被视为文本来解析。然后, math>会闭合元素,现在会在HTML名称空间中创建img,从而导致XSS。
6总结
以上绕过方式可以总结成以下几点:
DOMPurify的典型使用方法致使HTML标记被解析两次
。
HTML规范中存在特殊情况,可以创建嵌套form元素。但是,在重新解析时,
第二个form会被忽略
。
mglyph和malignm
ark
是HTML规范中的特殊元素,即使默认情况下其他所有标签都位于HTML命名空间中,但如果它们是MathML文本集成点的直接子元素,那么它们也将位于MathML命名空间中。
结合以上几点,我们可以创建一个具有两个form元素和mglyph元素的标记,这些标记最初位于HTML命名空间中,
但是在重新解析时位于MathML命名空间中,从而使后续的style标签的解析方式有所不同,导致XSS。
end
好文!必须在看