【前端安全】聊聊瀏覽器解析順序和 HTML 閉合優先級
最近在研究 XSS 的時候,發現一個特別容易被忽略的問題 —— 瀏覽器到底是怎么解析 HTML 的?為什么有些 payload 成功了,有些卻怎么試都不行?其實這跟標簽的閉合優先級還有解析順序有很大關系。
這篇文章就來聊聊這個問題,順便整理一下我踩過的坑和總結的規律。
一、瀏覽器是怎么解析 HTML 的?
我們都知道瀏覽器是按順序從上往下解析 HTML 的,但有一點容易忽略:
只要遇到 <script>
,瀏覽器就會暫停 DOM 的解析,優先執行里面的 JS。
也就是說,DOM 結構和 JS 是交替解析的,而不是一起解析的。
舉個栗子:
<p>前面的標簽</p>
<script>alert('中間插入了一段 JS');
</script>
<p>后面的標簽</p>
在 alert()
執行完之前,后面的 <p>
標簽都不會被解析進 DOM 樹里。
二、HTML 和 JS 中的“編碼解析”
HTML 中能解析的編碼
HTML 中的屬性值,比如 <img>
的 src
、onerror
,是可以解析一些編碼的:
比如:
<img src=x onerror=alert(1)>
a
實際上是字符 a
,所以這里最終會執行 alert(1)
。
更復雜一點的:
<img src="1"onerror=\u0061\u006c\u0065\u0072\u0074('\u0031')>
這是一堆 Unicode 轉義,瀏覽器會還原成 JS 代碼。雖然看著很花,但其實本質還是在執行 alert('1')
。
JS 里的編碼也能玩花樣
<script>
\u0061lert("<HelloWorld>");
</script>
\u0061
是 a
,所以這行代碼其實就是 alert("<HelloWorld>")
。
這些“編碼繞過”技巧在 XSS 中經常用到,尤其是某些過濾器只過濾了關鍵詞,但沒處理 Unicode 或 HTML 實體的時候,簡直不要太好用。
三、結構性字符不可用編碼繞過
有些結構性字符,是不能輕易編碼的,否則瀏覽器會把它當成“普通值”,根本不當回事。
字符 | 作用 |
---|---|
" | 屬性值起始/結束 |
' | 屬性值起始/結束 |
= | 屬性賦值符號 |
< | 標簽起始 |
> | 標簽結束 |
/ | 結束標簽的斜杠 |
空格 | 屬性之間的分隔 |
比如下面這個例子就失敗了:
<img src="" onerror=alert(1) "">
你以為你寫了 onerror=alert(1)
,但瀏覽器根本不認,它只會當成一個超長的 src
值。
四、比雙引號閉合優先級更高的標簽
有一類標簽,你一旦打開,里面寫啥都不會被解析成 HTML 標簽或屬性,直到你顯式地把它關閉。
這些標簽包括:
<!--
<iframe>
<noframes>
<noscript>
<script>
<style>
<textarea>
<title>
<xmp>
舉個真實的 payload:
<script>var a="</script><script>alert(1);var a=""</script>
在第一個 <script>
中的字符串沒閉合,導致后面的 </script>
被當成字符串的一部分吃掉了,瀏覽器繼續往下讀,直到遇到下一個 <script>
標簽,再繼續執行。于是 alert(1)
就偷偷溜進去了。
這種技巧經常被用來構造逃逸型 XSS,非常常見。
附一個好用的短xss payload網站
https://tinyxss.terjanq.me/