浏览器解析机制-XSS 入门

浏览器解析机制

HTML 解析

HTML 解析器使用状态机机制进行解析, 首先从输入流获取字符并按照规则转换到另一种状态

在这里可以看到所有的状态: https://html.spec.whatwg.org/multipage/parsing.html#tokenization


首先了解一些概念:

字符实体 (character entities)

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

HTML字符实体 (HTML character entities)

在HTML中, 某些字符是预留的. 例如在HTML中不能直接使用 <>, 这是因为浏览器可能误认为它们是标签的开始或结束. 如果希望正确地显示预留字符, 就需要在HTML中使用对应的字符实体, 比如 < 对应的实体为&lt;

字符引用(character references)

字符引用包括 字符值引用 和 字符实体引用. < 对应的字符值引用为 &#60; 对应的字符实体引用为 &lt; . 字符实体引用也被叫做 实体引用 或 实体


这里只讨论如下规则:

初始状态为数据状态 (data state) 处理普通文本字符, 直到遇到 < 会进入标签打开状态 (tag open state)

  • 当解析器遇到 & 符号进入字符引用状态 (character reference state), 解析字符引用
  • 字符引用状态解析完成后, 会回到上一个状态
  • 遇到 < 符号进入标签打开状态 (tag open state)
  • 在标签打开状态遇到 ascii 字母进入标签名状态 (tag name state), 读取标签名
  • 在标签打开状态遇到 / 进入结束标签状态 (end tag open state)
  • 标签名状态遇到 space 进入前属性名状态 (before attribute name state), 读取属性名, 这个状态会跳过 space
  • 在标签名状态遇到 / 进入自关闭开始标签状态 (self-closing start tag state)
  • 在标签名状态遇到 > 切换到数据状态 (data state)
  • 前属性名状态进入属性名状态 (attribute name state), 读取属性名
  • 属性名状态遇到遇到 = 进入前属性值状态 (before attribute name state), 准备读取属性值
  • 前属性值状态遇到遇到 "' 进入双引号属性值状态 (attribute value (double-quoted) state) 或单引号属性值状态 (attribute value (single-quoted) state), 读取属性值
  • 在单双引号属性值状态遇到使其闭合的单双引号进入后引号属性值状态 (after attribute value (quoted) state)
  • 在单双引号属性值状态遇到 & 进入字符引用状态
  • 在后引号属性值状态遇到 space 进入前属性名状态
  • 在后引号属性值状态遇到 / 进入自关闭开始标签状态
  • 在后引号属性值状态遇到 > 进入数据状态
  • 在自关闭开始标签状态遇到 > 进入数据装崖

例如这个标签: <input name="s"/>:

  1. 解析器读取到一个 < 进入标签打开状态
  2. 遇到了 ascii 字母 i 进入标签名状态
  3. 进入前属状态准备读取属性名, 解析器会跳过空白字符并检查下一个字符是否是有效的属性名的开始
  4. 进入属性名状态读取属性名
  5. 遇到 = 进入前属性值状态
  6. 遇到 " 进入双引号属性值状态, 读取属性值
  7. 遇到第二个 " 进入后引号属性值状态
  8. 遇到 / 进入自关闭开始标签状态
  9. 遇到 > 又回到数据状态

字符引用解析

这里单独说明一下字符引用解析, 先看英文原文:

When a state says to flush code points consumed as a character reference, it means that for each code point in the temporary buffer (in the order they were added to the buffer) user agent must append the code point from the buffer to the current attribute's value if the character reference was consumed as part of an attribute, or emit the code point as a character token otherwise.

上面这段话描述的就是: 在字符引用解析完成后, 如果是属性的一部分, 那么将解析后的值添加到该属性的值中; 而不是属性的一部分, 而是作为普通文本内容被解析, 那么这些代码点会作为字符 token 发出, 加入到 DOM 树中

示例 1: 作为属性的一部分

假设有以下 HTML 片段:

<a href="https://example.com?value=1&amp;other=2">Link</a>

解析器处理 href 属性时:

  1. 遇到字符引用 &amp
  2. &amp; 转换为 &, 并将其代码点放入临时缓冲区
  3. 指示 "flush code points consumed as a character reference"
  4. 解析器将缓冲区中的 & 添加到 href 属性的值中, 因此属性值变为 https://example.com?value=1&other=2

示例 2: 作为普通文本内容

假设有以下 HTML 片段:

<p>This &amp; that</p>

解析器处理文本内容时:

  1. 遇到字符引用 &amp
  2. &amp 转换为 &,并将其代码点放入临时缓冲区。
  3. 指示 "flush code points consumed as a character reference"
  4. 解析器将缓冲区中的 & 作为字符令牌发出, 并将其添加到 <p> 元素的文本内容中, 因此文本内容变为 This & that

再看这一规则:

The exact behavior of certain states depends on the insertion mode and the stack of open elements. Certain states also use a temporary buffer to track progress, and the character reference state uses a return state to return to the state it was invoked from.

字符引用状态在解析完成成后会回到调用他的状态, 比如从数据状态进入字符引用状态后会回到数据状态


在上面讨论了 data state 的情况, 除了 data state, 还有 rcdata state

Data state

  • 浏览器的默认状态
  • 普通的字符会直接解析为文本节点, 添加到 DOM 树中
  • 解析标签
  • 解析字符引用

RCDATA state

  • 用于解析特定元素, 如 <textarea>, <title>
  • 解析普通字符
  • 不会解析标签
  • 解析字符引用

所以, 对于 Data state 可以执行脚本, 但 RCDATA state 不行

那么什么时候只能容纳文本, 什么时候可以容纳标签呢?

HTML 有如下五类元素:

  • 空元素 (Void elements), 不能容纳任何内容. 如 <area>, <br>, <base>
  • 原始文本元素 (Raw text elements), 可容纳文本. 如 <script>, <style>
  • RCDATA 元素 (Raw Character Data elements), 可容纳文本和字符引用. 如 <textarea>, <title>
  • 外部元素 (Foreign elements), 可容纳文本, 字符引用, CDATA 段, 其他元素和注解. 如 <svg>
  • 基本元素 (Normal elements), 可容纳文本, 字符引用, 除以上元素的所有元素

URL 解析

URL 解析器同样顺传状态机模型. 解析规则: https://url.spec.whatwg.org/

URL 资源类型必须是 ascii 字母, 否则就会进入无类型状态, 就不会被 URL 解析器识别

URL 编码过程使用 UTF-8 来编码每一个字符. 如果使用了其他编码类型可能不会正确识别

JavaScript 解析

JavaScript 与上述的解析过程不一样, 可以使用内容无关语法来解释 JavaScript 如何解析. ECMAScript-262: https://ecma-international.org/publications-and-standards/standards/ecma-262/

在上文 HTML 解析过程中, <script> 属于原始文本元素, 所以在 <script> 块中的字符引用并不会被解码和解析

对于特殊的 Unicode 转义序列 (如 \u0000, \u000A), 有三种情况:

  • 在字符串中

    ECMAScript 中只会被解释为正规字符, 并不会解释为带单双引号或换行符这些能够打破字符串上下文的字符. Unicode 转义序列将永远不会破坏字符串上下文, 因此它们只能被解释成字符串常量

  • 标识符名称中 (函数名, 属性名等)

    会被解码并解释为标识符名称的一部分

  • 控制字符

    当表示一个控制字符, 如单双引号, 圆括号等, 它们将不会解释为控制字符, 而仅仅被解码并解析为标识符名称或字符串常量. 例如解析器在解析一个函数调用时, 如 alert(), 圆括号必须是 (), 不能是 \u0028 \u0029

解析流

当浏览器从网络堆栈中获取一段内容后, 先使用 HTML 解析器对这段被荣进行词法解析, 在这一过程中字符引用将被解码. 词法解析完成后, DOM 树被创建好了, 然后 JavaScript 解析器会对内联脚本进行解析, 在这一步中 Unicode 转义序列和 Hex 转义序列将被解码. 如果遇到需要 URL 的上下文, 会使用 URL 解析器进行解析, 由于 URL 的位置不同, URL 解析器可能会在 JavaScript 解析器之前或者之后使用

XSS Payload 例子分析

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

encoded

javascript:alert(1)

不会执行

根据 URL 解析规则, 资源类型只能是 ascii 字符, URL 解析器无法识别

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

会执行

根据解析流顺序, HTML 先解析, 在双引号属性值状态中解析字符引用, 解析为 javascript:%61%6c%65%72%74%28%32%29, 可以被 URL 解析器解析, 由于是 javascript 协议, URL 解析器解析为 javascript:alert(1) 交给 JavaScript 解析器后被执行

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

不会执行

同 1

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

不会执行

以开发者的角度思考, HTML 编码就是为了显示这些特殊字符, 而不干扰正常的 DOM 解析, 所以这里面的内容不会变成一个 img 元素, 也不会被执行

从 HTML 解析机制来看, 进入字符引用状态后不会进入标签开始状态, 而是回到调用他的状态

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

不会执行

RCDATA 元素只能容纳文本和字符引用, 不能容纳其他元素

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

不会执行

同 5

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

会执行

&#39 被解释为 ', 类比 2

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

不会执行

根据 JavaScript 解析规则, \u0027 表示控制字符 ', 不会解码

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

不会执行

<script> 为原始文本元素, 只能容纳文本

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

会执行

这里的 \u0061\u006c\u0065\u0072\u0074 表示标识符 alert, 可以解码为标识符的一部分

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

同 8

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

不会执行

\u0031\u0032 在解码的时候会被解码为字符串 12, 也就变成了 alert(12), 但是这里的 12 不是数字是字符串需要引号, 所以 js 解析失败

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

不会执行

同 8

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

会执行

会解析为字符常量 \n, 并不会导致代码换行

<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;"></a>

会执行

先 HTML 解析, 得到: javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15), 再由 URL 解析得到 javascript:\u0061\u006c\u0065\u0072\u0074(15), 识别为 js 协议, 由 js 解析得到 javascript:alert(15)