文章目录
浏览器的工作流程
浏览器的主要构成
- 用户界面- 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分
- 浏览器引擎- 用来查询及操作渲染引擎的接口
- 渲染引擎- 用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来
- 网络- 用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作
- UI 后端- 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
- JS解释器- 用来解释执行JS代码
- 数据存储- 属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术
浏览器的编码解码过程主要与渲染引擎和JS解释器有关,所以下面会重点讨论渲染引擎和JS解释器的工作流程。
浏览器的渲染引擎和解释器
这是浏览器从接收代码,到渲染完成的过程,从开头我们能看到它有三个主要部分:
- HTML/SVG/XHTML 解析,事实上,Webkit有三个C++的类对应这三类文档。解析这三种文件会产生一个DOM Tree。
- CSS 解析,解析CSS会产生CSS规则树。
- Javascript DOM,主要是通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree.
简单的来说浏览器接受到内容后首先进行html解析,首先将识别到的标签转化为DOM树,而在这一过程中浏览器是无法识别html实体编码的,只有建立起DOM树才能对每个节点的内容进行识别,如果出现html实体编码,则会进行解码。
举个简单的例子,在XSS中如果页面可以通过用户输入的内容构造新标签,此时为了绕过过滤进行html编码。s
编码为s
,那么形如<img src=0 onerror=alert(1)
这种payload是无法正常运行的,因为破坏了标签内的属性名src导致html解析时不会正确识别img标签无法进行DOM树构建,自然也就无法进行html实体解码过程。具体的构建DOM树的过程在下文浏览器的解析流中会具体阐述。
在此基础上,JavaScript DOM API 参与进来,可以对DOM 树进行修改,改变DOM树的结构和内容。而此时,CSS解析器则解析外部CSS 文件以及Style 标签中的样式内容,这些信息将搭配HTML 中的可见指令构建起一个Rendering Tree。
Render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。
Render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。
这个视频能形象的看到布局的过程------Gecko reflow visualization
值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
浏览器中的编码类型
要了解XSS中浏览器的解码顺序要先对几种编码的形式以及html,url,js引擎所能识别的符号有基本的了解。
url编码
一般来说,url只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。因为有些字符在url中有特殊的含义,所以不能用于url当中。
url编码方案主要是用于对拓展ASCII字符集中的任何有问题的字符进行编码,使其可以通过HTTP安全传输。任何url编码的字符都已%
为前缀,其后是这个字符的十六进制ASCII代码。以下为一些常见的url编码字符。
编码 | 符号 |
---|---|
%20 | 空格 |
%27 | 单引号 |
%3C | < |
%3E | > |
%28 | ( |
%29 | ) |
%3d | = |
%0a | 换行 |
%00 | 空字节 |
+ | 空格 |
html实体编码
HTML编码是一种用于表示问题字符以将其安全并入HTML文档的方案。有许多字符有特殊的含义(如<>用于闭合标签),并被用于定义文档结构而非内容。为了安全使用这些字符,必须进行HTML编码。常见的html编码。
编码 | 符号 |
---|---|
< | < |
> | > |
& | & |
" | " |
&apos | ’ |
此外任何字符都可以使用它的十进制ASCII码进行HTML编码,例如
编码 | 符号 |
---|---|
" | " |
' | ’ |
或者使用十六进制ASCII码(以x为前缀),例如
编码 | 符号 |
---|---|
" | " |
' | ’ |
unicode编码
因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特(bit)作为一个字节(byte)。一个字节能表示的最大的整数就是255(2^8-1=255),而ASCII编码,占用0 - 127用来表示大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A的编码是65,小写字母z的编码是122。
如果要表示中文,显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。
类似的,日文和韩文等其他语言也有这个问题。为了统一所有文字的编码,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。
浏览器的解析流
URL解析
在这些所有工作流程开始之前,浏览器一定需要有一个URL 来指示资源的位置,为什么刚才没有说呢,因为这个URL 是浏览器发送给服务器的请求信息,其处理工作并不是浏览器的工作。比如我们考虑一段简单的代码:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<a href="javascript:alert('<?php echo $_GET['input'];?>');">test</a>
</body>
</html>
input内容是:%26lt%5cu4e00%26gt
该值构造在URL 里,浏览器直接发送给服务器,服务器接收之后,先进行URL 解析,看到了% 这个符号,于是URL 解码(%26对应&符号),input 内容变成了<\u4e00>
,所以对于浏览器从服务器端获取的页面数据来说,此时test 对应的标签变成了如下:
<a href="javascript:alert('<\u4e00>');">test</a>
这一步完成在所有的工作之前,URL 解码发生在第一部,而且它基本上都发生在服务器上。
XSS中url编码的位置:
URL资源类型必须是ASCII字母(U+0041-U+005A || U+0061-U+007A),不然就会进入“无类型”状态。例如,你不能对协议类型进行任何的编码操作,不然URL解析器会认为它无类型。
对javascript协议编码:
<a href="%6a%61%76%61%73%63%72%69%70%74:alert(1)"></a>
对:编码:
<a href="javascript%3Aalert(1)"></a>
上面展示两个例子都是无法正常解析的,原因为资源类型javascript:被破坏了
<a href="javascript:%61%6c%65%72%74%28%31%29"></a>
url编码alert(1)
这种形式则会被正确解析
html解析
html的解析流程如下图:
整个解析算法分为两个流程符号化和构建树。
符号化
符号化是词法分析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名及属性值。
符号识别算法 The tokenization algorithm
算法输出html符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。
算法本身很复杂,用一个简单的例子来描述,符号化下面的html
<html>
<body>
Hello world
</body>
</html>
初始状态为“Data State”,当遇到“<”字符,状态变为“Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为“Tag name state”,一直保持这个状态直到读取到“>”,每个字符都附加到这个符号名上,例子中创建的是一个html符号。
当读取到“>”,当前的符号就完成了,此时,状态回到“Data state”,<body>
重复这一处理过程。到这里,html和body标签都识别出来了。现在,回到“Data state”,读取“Hello world”中的字符“H”将创建并识别出一个字符符号,这里会为“Hello world”中的每个字符生成一个字符符号。
这样直到遇到</body>
中的“<”。现在,又回到了“Tag open state”,读取下一个字符“/”将创建一个闭合标签符号,并且状态转移到“Tag name state”,还是保持这一状态,直到遇到“>”。然后,产生一个新的标签符号并回到“Data state”。后面的</html>
将和</body>
一样处理。
构建树
符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。
树的构建算法 Tree construction algorithm
在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。
来看一下示例中树的创建过程:
<html>
<body>
Hello world
</body>
</html>
构建树这一阶段的输入是符号识别阶段生成的符号序列。
首先是“initial mode”,接收到html符号后将转换为“before html”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。
状态此时变为“before head”,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。
现在,转到“in head”模式,然后是“after head”。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“in body”模式。
然后,接收到字符串“Hello world”的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。
接收到body结束符号时,转移到“after body”模式,接着接收到html结束符号,这个符号意味着转移到了“after after body”模式,当接收到文件结束符时,整个解析过程结束。
XSS中html实体编码的位置
在符号化过程中,html的解析器只能识别出特定的词法规则,才能成功构建出DOM节点,在识别符号的过程中不会进行任何的html实体解码。符号化的过程中需要依赖开始标签,结束标签,属性名来构造一个完整的节点,所以尝试在这三个位置进行html编码绕过将导致节点无法正常解析,比如下面几个例子。
<!--标签被编码 g实体编码g-->
<img src=1 onerror="alert('img标签名被编码')">
<!--属性名被编码 s实体编码s-->
<img src=1 οnerrοr=alert('scr属性被编码')>
<!--响应事件名被编码 o实体编码o-->
<img src='' onerror=alert('响应事件名onclick被编码')>
<!--响应事件属性值被编码(可以弹窗)! a实体编码a-->
<img src=1 οnerrοr=alert('响应事件体alert被编码')>
打开后只有响应事件值被实体编码的img标签可以正常弹窗。
在DOM 树构建完毕之后,这些HTML 实体编码的内容就会被解码。
此时继续看上面的例子:
经过html实体的解析后,<
变为了<,>
变为了>
<a href="javascript:alert('<\u4e00>');">test</a>
CSS解析
一般来说,CSS 解析器会做接下来的工作,不过一般来说,为了考虑到更好的体验和性能,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
当然CSS不会干扰到DOM 树的建立,他会结合CSS文件和style 标签,以及HTML中的课件指令来构建起reder tree。这里JavaScrit 的 CSSOM api 也会出一些力。
CSS 编码解析是用了一套不太正统的转义策略:用一个反斜杠,后边跟1~6位十六进制数字构成。所以字母e 可以编码为 \65, \065,\000065。而因为这样,后边就不能直接紧跟数字或字母,否则会被当成转义里的内容处理,所以CSS 选择了空格作为终止标识,在解码的时候,再将空格去除。
同时,CSS还支持直接使用反斜杠对非十六进制字符进行转义的方式,就按紧跟着反斜杠后边的字符的字面意思进行解释,这种机制可用来转义引号和反斜杠本身,不过不能转义HTML 控制的字符,比如尖括号,那是因为HTML 解析器总是先于CSS 解析器。
由于CSS 转义规定的语焉不详,许多解析器会对本该用引号括起来的字符串进行任意的转义,特别的,在IE 浏览器里,这种转义优先级高于伪函数语法,于是下边两种情况的写法是一样的:
olor:expression(alert(1))
color:expression\028 alert \028 1 \029 \029
JS解析
上边提到了style ,是建立render tree 的时候使用的,它怎么工作的呢。考虑到我们的浏览器为了让不同的解析器来工作处理不同的内容,实际上,在处理诸如< script>
这样的标签,解析器会自动切换到特殊解析模式,而src href 后边加入的JavaScript 伪URL,也会进入JS 的解析模式。而进入该解析模式的时候,该DOM节点已经建立起来了。
还是刚刚的例子:
经过url和html解析 原本的input:%26lt%5cu4e00%26gt
已经变为了
<a href="javascript:alert('<\u4e00>')">test</a>
此时浏览器解析到href标签值发现了javascript:
,调用javascript解析器进行解析,里边有一个转义字符\u4e00,前导的 \u 表示他是一个Unicode 字符,根据后边的数字,解析为’一’,于是在完成JS的解析之后变成了:
<a href="javascript:alert('<一>')">test</a>
故网页最后显示的是:
在一个页面中可以触发JS解析器的有这么几种
- 直接嵌入< script> 代码块。
- 通过< script sr=… > 加载代码。
- 各种HTML CSS 参数支持JavaScript:URL 触发调用。
- CSS expression(…) 语法和某些浏览器的XBL 绑定。
- 事件处理器(Event handlers),比如 onload, onerror, onclick等等。
- 定时器,Timer(setTimeout, setInterval)
- eval(…) 调用。
XSS中Unicode编码的位置
与CSS松散的规则不同,对于JavaScript,转义编码应当只出现在标示符部分,不能用于对语法有真正影响的符号,也就是括号,或者是引号。通过几个例子来探究js引擎可以正确解析的编码位置。
<!--函数名被编码-->
<a href="javascript:\u0061lert('函数名被编码')">test</a>
<!--()被编码-->
<a href="javascript:alert\u0028'圆括号被编码'\u0029">test</a>
<!--'被编码-->
<a href="javascript:alert(\u0027引号被编码\0027')">test</a>
经过测试发现只有函数名被编码可以正常弹窗。
当编码位置在字符串中时,它只会被解释为正规字符,而不是单引号,双引号或者换行符这些能够打破字符串上下文的字符。这项内容清楚地写在ECMAScript中。因此,Unicode转义序列将永远不会破环字符串上下文,因为它们只能被解释成字符串常量。
<script>\u0061\u006c\u0065\u0072\u0074(\u0031\u0032)</script>
<script>\u0061\u006c\u0065\u0072\u0074('\u0031\u0032')</script>
Unicode 编码 alert 和 12
上面两段代码只有2会执行 1并不会执行 因为括号内的\u0031\u0032
并不会被解释为字符串常亮(因为它们没有用引号闭合)
<script>alert('13\u0027)</script>
同理这个例子中的\u0027
也不会被解释为 ’ 所以单引号没有闭合 同样无法执行
解析顺序
在讨论过各种解析形式后,最后来讨论这些解析的先后顺序。
当浏览器从网络堆栈中获得一段内容后,触发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。
最后的例子
<a href="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)"></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)"
变为:
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)
javascript:触发了最后的js解析
javascript:alert(15)
参考文章:
编码与解码
深入理解浏览器解析机制和XSS向量编码
How browsers work