本文内容:

从浏览器编码解码的角度看待XSS是怎么产生的以及我们应该怎么防御XSS

浏览器的编码与解码

浏览器对一个HTML文档进行解析的时候,会有HTML解析器、URL解析器以及JavaScript解码器同时工作参与解码的任务

HTML解码器

作为HTML解析器,它实际上是以状态机的形式存在,下面是HTML解析器在词性分析方面的一个WiKi,受限于国内环境,打开可能会有点缓慢,建议挂梯子打开

https://html.spec.whatwg.org/multipage/parsing.html

一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。

在解析过程中,任何时候它只要遇到一个’<’符号(后面没有跟’/‘符号<>&"')就会进入“标签开始状态(Tag open state)”。

然后转变到“标签名状态(Tag name state)”,“前属性名状态(before attribute name state)”……最后进入“数据状态(Data state)”并释放当前标签的token

当解析器处于“数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token

这里有三种情况可以容纳字符实体,“数据状态中的字符引用”,“RCDATA状态中的字符引用”和“属性值状态中的字符引用”。在这些状态中HTML字符实体将会从“&#…”形式解码,对应的解码字符会被放入数据缓冲区中。

当一个标签后面的如<div>后面的&#60; 被解码后,实际上会产生<,但此时不会进入标签开始状态,因为如此,就不会建立新标签。因此,我们能够利用字符实体编码这个行为来转义用户输入的数据从而确保用户输入的数据只能被解析成“数据”。

字符实体(character entities)

字符实体是一个转义序列,它定义了一般无法在文本内容中输入的单个字符或符号。一个字符实体以一个&符号开始,后面跟着一个预定义的实体的名称,或是一个#符号以及字符的十进制数字。

HTML字符实体(HTML character entities)

通常会对<、>、&、"、'做实体处理

原始符号 实体名称 实体编码
< &lt; &#60;
> &gt; &#62;
& &amp; &#38;
&quot; &#34;
&apos; &#39;

在HTML中有五类元素:

  1. 空元素(Void elements),如<area>,<br>,<base>等等

  2. 原始文本元素(Raw text elements),有<script><style>

  3. RCDATA元素(RCDATA elements),有<textarea><title>

  4. 外部元素(Foreign elements),例如MathML命名空间或者SVG命名空间的元素

  5. 基本元素(Normal elements),即除了以上4种元素以外的元素

五类元素的区别如下:

  1. 空元素,不能容纳任何内容(因为它们没有闭合标签,没有内容能够放在开始标签和闭合标签中间)。
  2. 原始文本元素,可以容纳文本。
  3. RCDATA元素,可以容纳文本和字符引用。
  4. 外部元素,可以容纳文本、字符引用、CDATA段、其他元素和注释
  5. 基本元素,可以容纳文本、字符引用、其他元素和注释

对RCDATA有个特殊的情况

在浏览器解析RCDATA元素的过程中,解析器会进入“RCDATA状态”。

在这个状态中,如果遇到“<”字符,它会转换到“RCDATA小于号状态”。如果“<”字符后没有紧跟着“/”和对应的标签名,解析器会转换回“RCDATA状态”。这意味着在RCDATA元素标签的内容中(例如<textarea><title>的内容中),唯一能够被解析器认做是标签的就是“</textarea>”或者“</title>

URL解码器

首先,URL资源类型必须是ASCII字母(U+0041-U+005A || U+0061-U+007A),不然就会进入“无类型”状态。

例如,你不能对协议类型进行任何的编码操作,不然URL解析器会认为它无类型。

同时,URL编码过程使用UTF-8编码类型来编码每一个字符。

如果你尝试着将URL链接做了其他编码类型的编码,URL解析器就可能不会正确识别。

Javascript解码器

所有的“script”块都属于“原始文本”元素。

script”块有个有趣的属性:在块中的字符引用并不会被解析和解码。

如果你去看“脚本数据状态”的状态转换规则,就会发现没有任何规则能转移到字符引用状态。

所以如果攻击者尝试着将输入数据编码成字符实体并将其放在script块中,它将不会被执行。

标识符名称中:

当Unicode转义序列出现在标识符名称中时,它会被解码并解释为标识符名称的一部分,例如函数名,属性名等等。

控制字符:

当用Unicode转义序列来表示一个控制字符时,例如单引号、双引号、圆括号等等,它们将不会被解释成控制字符,而仅仅被解码并解析为标识符名称或者字符串常量。如果你去看ECMAScript的语法,就会发现没有一处会用Unicode转义序列来当作控制字符。例如,如果解析器正在解析一个函数调用语句,圆括号部分必须为“(”和“)”,而不能是\u0028\u0029

浏览器解析顺序

当浏览器从网络堆栈中获得一段内容后,触发HTML解析器来对这篇文档进行词法解析

在这一步中字符引用被解码。

在词法解析完成后,DOM树就被创建好了,JavaScript解析器会介入来对内联脚本进行解析。

在这一步中Unicode转义序列和Hex转义序列被解码。

同时,如果浏览器遇到需要URL的上下文,URL解析器也会介入来解码URL内容。

在这一步中URL解码操作被完成。

由于URL位置不同,URL解析器可能会在JavaScript解析器之前或之后进行解析。

考虑如下两种情况:

Example A:<a href="UserInput"></a>
Example B: <a href=# onclick="window.open('UserInput')"></a>

在例A中,HTML解析器将首先开始工作,并对UserInput中的字符引用进行解码。然后URL解析器开始对href值进行URL解码。最后,如果URL资源类型是JavaScript,那么JavaScript解析器会进行Unicode转义序列和Hex转义序列的解码。再之后,解码的脚本会被执行。因此,这里涉及三轮解码,顺序是HTML,URL和JavaScript。

在例B中,HTML解析器首先工作。然而接下来,JavaScript解析器开始解析在onclick事件处理器中的值。这是因为在onclick事件处理器中是script的上下文。当这段JavaScript被解析并被执行的时候,它执行的是“window.open()”操作,其中的参数是URL的上下文。在此时,URL解析器开始对UserInput进行URL解码并把结果回传给JavaScript引擎。因此这里一共涉及三轮解码,顺序是HTML,JavaScript和URL。

几个小练习

Question 1

<a href="%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29"></a>

Clickme
不能对协议类做任何的编码操作,否则URL编码器会任务他是无类型的

Question 2

<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;:%61%6c%65%72%74%28%32%29">Click me</a>

Click me

这时候&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;:%61%6c%65%72%74%28%32%29处于属性值下的字符应用

这时候HTML解码器会尝试对前半部分的&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;进行HTML解码

此时href中的javascript:%61%6c%65%72%74%28%32%29会被URL解码器进行解码

最后由于是Javascript协议,Javascript解析器会开始工作,从而执行JS脚本

Question 3

<a href="javascript%3aalert(3)">Click me</a>

Click me

同时对于冒号而言,不能进行编码处理,否则同样会被URL编码器认定为无类型

Question 4

<div>&#60;img src=x onerror=alert(4)&#62;</div>
<img src=x onerror=alert(4)>

在解析完<div>标签后,后面的&#60;会被解析为<,但此时不会再进入标签开始状态,导致新的标签不会建立

Question 5

<textarea>&#60;script&#62;alert(5)&#60;/script&#62;</textarea>

RCDATA元素内只会解析<textarea>和<title>作为标签,其他情况只会认定为RCDATA小于号状态,而不是标签状态,所以不会解析

Question 6

<textarea><script>alert(6)</script></textarea>

原因同上

Question 7

<button onclick="confirm('7&#39;);">Button</button>


这里先经过HTML解码,然后会恢复成',由于onclick的存在,JS解析器成功执行弹窗

Question 8

<button onclick="confirm('8\u0027);">Button</button>

Javascript中,控制字符不能被Unicode编码,否则不会触发

Question 9

<script>&#97;&#108;&#101;&#114;&#116&#40;&#57;&#41;&#59</script>

script标签内进行编码解码都不会执行,只能输入原始语句

Question 10

<script>\u0061\u006c\u0065\u0072\u0074(10);</script>

当Unicode转义序列出现在标识符名称中时,它会被解码并解释为标识符名称的一部分,例如函数名,属性名等等

Question 11

<script>\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0031\u0029</script>

同上,控制字符不能被编码处理

Question 12

<script>\u0061\u006c\u0065\u0072\u0074(\u0031\u0032)</script>

这里\u0031\u0032不会被当做字符串常量,除非用引号包裹

Question 13

<script>alert('13\u0027)</script>

\u0027只会被解释为文本单引号,而不是控制字符

Question 14

<script>alert('14\u000a')</script>

这里\u000a会被解释为换行符文本,而不是控制字符,不会引起Javascript错误,从而会弹窗,实际上这时候弹的是14加上一个换行符文本

Question 15

<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x33;&#x31;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25;&#x36;&#x33;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x36;&#x25&#x33;&#x35;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x37;&#x25;&#x33;&#x32;&#x25;&#x35;&#x63;&#x25;&#x37;&#x35;&#x25;&#x33;&#x30;&#x25;&#x33;&#x30;&#x25;&#x33;&#x37;&#x25;&#x33;&#x34;&#x28;&#x31;&#x35;&#x29;">Click me</a>

Click me

这个会触发,这里仔细讲一下解码流程:

  1. 首先看到<a标签,知道要进行HTML解码了,对属性当中的字符引用进行解码
  2. 解码得到<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36&#x2535%5c%75%30%30%37%32%5c%75%30%30%37%&# x33;4(15)">Click me</a>
  3. 此时href看到javascript:协议,符合URL解码条件,开始URL解码
  4. 解码得到<a href="javascript:\u0061\u006c\u0065\u0072\u0074(15)">Click me</a>
  5. 这时候Javascript解析器开始工作,把后面的部分进行Unicode解码,由于括号等控制字符没有被Unicode编码,这里会解码后正常弹窗

最终解码过程:

image-20200327211803991

最后

那几个无法触发的弹窗,错误原因都是这几个

image-20200327211942977

无效的标记Hhhh