浏览器的解码规则
HTML解析器对HTML文档进行解析完成HTML解码并且创建DOM树
javascript 或者 CSS解析器对内联脚本进行解析,完成JS CSS解码
URL解码会根据URL所在的顺序不同而在JS解码前或者解码后
浏览器最早开始解析HTML,将标签转化为内容树中的DOM 节点,此时识别标签的时候,HTML 解析器是无法识别哪些被实体编码的内容的,只有建立起DOM 树,才能对每个节点的内容进行识别,如果出现实体编码,则会进行实体解码。在此基础上,JavaScript DOM API 参与进来,可以对DOM 树进行修改,改变DOM树的结构和内容。而此时,CSS解析器则解析外部CSS 文件以及Style 标签中的样式内容
编码与解码的顺序
url解析
在之前的解析之中,浏览器会先解析HTML,但是如何获取HTML等这些资源呢,还是先需要url来获取到资源的。
1 |
|
test的值构造在URL 里,浏览器直接发送给服务器,服务器接收之后,先进行URL 解析,看到了% 这个符号,于是URL 解码将内容变成<\u4e00>
。
这是最早的一步解析,在服务器上发生,与浏览器无关。
HTML解析
HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个<
符号(后面没有跟/
符号)就会进入“标签开始状态(Tag open state)”。然后转变到“标签名状态(Tag name state)”,“前属性名状态(before attribute name state)”……最后进入“数据状态(Data state)”并释放当前标签的token。当解析器处于“数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
最后生成一个dom树
1 | <html> |
所以这里 HTML只能识别特定的词法规则,才能构造起DOM树,这里的HTML是无法解码识别的,所以这一种的编码是无法识别出来的。
1 | <img src="https://0xtunan.github.io/img/avatar.png"> |
因为它破坏了标签的本身结构,无法生成DOM树。解码器识别到=
没法将他解码为=
所以无法执行。
但是这种是可以执行的
1 | <img src="https://0xtunan.github.io/img/avatar.png"> |
构造dom树之后,html会对进行解析。然后会把src中的东西丢到url中执行
1 | <a href="http://127.0.0.1/cs.php?test=%3C123">test</a> |
所以这个也是可行的,html实体先被转换,然后被丢到url中去请求在被服务器解析获取资源。
css解析
接下来就是由css来解析了,但是并不是HTML解析完成之后,css菜开始工作,而是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。但是css不会干扰到HTML DOM树的建造,他会结合CSS文件和style 标签 以及HTML标签的属性进行工作。
CSS编码的转义策略是:用一个反斜杠,后边跟1~6位十六进制数字构成。所以很多时候我们会发现这种编码
1 | /61 /061 /0061 /00061 /000061 |
而因为这样,后边就不能直接紧跟数字或字母,否则会被当成转义里的内容处理,所以CSS 选择了空格作为终止标识,在解码的时候,再将空格去除。同时,CSS还支持直接使用反斜杠对非十六进制字符进行转义的方式,就按紧跟着反斜杠后边的字符的字面意思进行解释,这种机制可用来转义引号和反斜杠本身,不过不能转义HTML 控制的字符,比如尖括号,那是因为HTML 解析器总是先于CSS 解析器
JS解析
在处理诸如< script> < style> 这样的标签,解析器会自动切换到特殊解析模式,而src href 后边加入的JavaScript 伪URL,也会进入JS 的解析模式。而进入该解析模式的时候,该DOM节点已经建立起来了。
1 | <a href="javascript:alert('\u0078\u0073\u0073')">test</a> |
在这里javascript:
之后就会调用,js的解析器,\u
就会被标记为Unicode字符被转义,但是不只是在引号中的字符能被辨识,可以看下面这两个
1 | <a href="javascript:\u0061\u006c\u0065\u0072\u0074('\u0078\u0073\u0073')">test</a>//正常弹窗 |
可以看到我们第一个也是可以弹窗的,而下面这两个是无法正常执行弹窗的。这是因为转义编码应当只出现在标示符部分,不能用于对语法有真正影响的符号,也就是括号,或者是引号。
在一个页面中,可以触发JS解析器的几种方式
- 直接嵌入< script> 代码块。
- 通过< script src=… > 加载代码。
- 各种HTML CSS 参数支持JavaScript:URL 触发调用。
- CSS expression(…)
- 事件处理器(Event handlers),比如 onload, onerror, onclick等等。
- 定时器,Timer(setTimeout, setInterval)
- eval(…) 调用。
demo
1 | <html> |
在这个里面最容易理解的是第一个
1 | <img src=# onerror="alert(1)" /> |
在这个里面,img标签被html解析,然后进行实体化转义,把a
转义为a,然后onerror触发js
代码。
在看下面这个img标签
1 | <img src=# onerror="\u0061lert(1)" /> |
我是先将其进行Unicode转码在进行html实体化编码的,可以看到它依旧是执行了出来,说明对于网页来说,它一般情况是先进行html转义,然后再进行js的解码。
再看下面这个两个标签
1 | document.getElementById("test").innerHTML = "<img src=# on\u0065rror=alert(2)>"; |
再看这两个代码中,由于html解析到了<script>
标签,所以它会先进行js的解码,把Unicode字符先转义,所以第一个是可以被执行的,但是第二个呢?第二个是html实体化编码,但是它还是被正常执行了,这是因为进入<script>
之后,浏览器先进行js的Unicode转义,将后面那一串字符变成a
也就是a的实体化转义字符,然后把该标签传回给HTML,HTML 建立DOM节点,再由HTML解码其中的内容,onerror 又会执行其中的JS 脚本,弹出窗口。
虽然有点绕,但是其实也不是那么难理解。需要注意的还是那些顺序问题,实在有不懂的,多实操一下,对比着想想其中的原因就差不多了。