阿里云上的域名總是讓我備案,實(shí)在是懶得備。還是用github上的博客吧,刀背藏身
無論是作為開發(fā),還是作為黑客,企圖從Web 端注入SQL,或者是XSS 的時(shí)候,編碼和解碼都是一個(gè)重要的問題、作為一個(gè)瀏覽器,有URL解析引擎,有HTML解析引擎,還有JS 解析引擎。其執(zhí)行的先后順序往往決定了輸出的結(jié)果。這種多標(biāo)簽語言互相嵌入的,同時(shí)又需要客戶端服務(wù)器交互的技術(shù),正是給了XSS 可趁之機(jī)。下面我們要做的,是去了解瀏覽器到底如何解碼,該如何在解碼過程中避免漏洞的產(chǎn)生。在此之上,我更愿意揭開整個(gè)瀏覽器的工作流程,了解其本質(zhì)。
這里有篇神文How browsers work, 也是本篇文章的重要參考。
瀏覽器基本的工作流程
進(jìn)入主話題之前,先閑扯一些廢話,先羅列一下瀏覽器的主要構(gòu)成:
- 用戶界面- 包括地址欄、后退/前進(jìn)按鈕、書簽?zāi)夸浀?,也就是你所看到的除了用來顯示你所請求頁面的主窗口之外的其他部分
- 瀏覽器引擎- 用來查詢及操作渲染引擎的接口
- 渲染引擎- 用來顯示請求的內(nèi)容,例如,如果請求內(nèi)容為html,它負(fù)責(zé)解析html及css,并將解析后的結(jié)果顯示出來
- 網(wǎng)絡(luò)- 用來完成網(wǎng)絡(luò)調(diào)用,例如http請求,它具有平臺無關(guān)的接口,可以在不同平臺上工作
- UI 后端- 用來繪制類似組合選擇框及對話框等基本組件,具有不特定于某個(gè)平臺的通用接口,底層使用操作系統(tǒng)的用戶接口
- JS解釋器- 用來解釋執(zhí)行JS代碼
- 數(shù)據(jù)存儲(chǔ)- 屬于持久層,瀏覽器需要在硬盤中保存類似cookie的各種數(shù)據(jù),HTML5定義了web database技術(shù),這是一種輕量級完整的客戶端存儲(chǔ)技術(shù)
其組件架構(gòu)是這樣的:

值得一提的是,對于Chrome 瀏覽器來說,Chrome 為每個(gè)Tab 都分配了各自的渲染引擎,每個(gè)Tab 都是一個(gè)獨(dú)立的進(jìn)程。
實(shí)際上,我們重點(diǎn)關(guān)注的就是其中的Rendering engine 和 JavaScript Interpreter ,渲染引擎和解釋器。說了那么多廢話,我們開始了解瀏覽器的主流程。

這是瀏覽器從接收代碼,到渲染完成的過程,從開頭我們能看到它有三個(gè)主要部分:
- HTML/SVG/XHTML 解析,事實(shí)上,Webkit有三個(gè)C++的類對應(yīng)這三類文檔。解析這三種文件會(huì)產(chǎn)生一個(gè)DOM Tree。
- CSS 解析,解析CSS會(huì)產(chǎn)生CSS規(guī)則樹。
- Javascript DOM,主要是通過DOM API和CSSOM API來操作DOM Tree和CSS Rule Tree.
瀏覽器最早開始解析HTML,將標(biāo)簽轉(zhuǎn)化為內(nèi)容樹中的DOM 節(jié)點(diǎn),此時(shí)識別標(biāo)簽的時(shí)候,HTML 解析器是無法識別哪些被實(shí)體編碼的內(nèi)容的,只有建立起DOM 樹,才能對每個(gè)節(jié)點(diǎn)的內(nèi)容進(jìn)行識別,如果出現(xiàn)實(shí)體編碼,則會(huì)進(jìn)行實(shí)體解碼。在此基礎(chǔ)上,JavaScript DOM API 參與進(jìn)來,可以對DOM 樹進(jìn)行修改,改變DOM樹的結(jié)構(gòu)和內(nèi)容。而此時(shí),CSS解析器則解析外部CSS 文件以及Style 標(biāo)簽中的樣式內(nèi)容,這些信息將搭配HTML 中的可見指令構(gòu)建起一個(gè)Rendering Tree。
這里CSS 解析器在構(gòu)造Redering Tree 之前,為了輔助會(huì)有CSS Rule Tree,他是為了完成匹配,然后把CSS Rule 附加給Rendering Tree 上的每個(gè)element,也就是每個(gè)DOM 節(jié)點(diǎn)。其中有一個(gè)layout/reflow 的過程,就是為了計(jì)算每個(gè)frame 位置等信息。
當(dāng)然,個(gè)人并不是一個(gè)Web 開發(fā)者,無意在CSS 這一塊花費(fèi)巨大的時(shí)間,下面這個(gè)視頻會(huì)很形象的讓你感受到layout/reflow 的過程。Gecko reflow visualization
完成布局之后,使用UI 后端完成每個(gè)節(jié)點(diǎn)的繪制,完成顯示。
編碼和解碼發(fā)生的順序
在看完瀏覽器工作流程之后(當(dāng)然,這個(gè)流程講的有點(diǎn)簡單了),我們來看一下編碼和解碼的順序,對應(yīng)著工作流程就很容易記清楚了。
URL 解析
在這些所有工作流程開始之前,瀏覽器一定需要有一個(gè)URL 來指示資源的位置,為什么剛才沒有說呢,因?yàn)檫@個(gè)URL 是瀏覽器發(fā)送給服務(wù)器的請求信息,其處理工作并不是瀏覽器的工作。比如我們考慮一段簡單的代碼:
<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 內(nèi)容參數(shù)為: %26lt%5cu4e00%26gt
該值構(gòu)造在URL 里,瀏覽器直接發(fā)送給服務(wù)器,服務(wù)器接收之后,先進(jìn)行URL 解析,看到了% 這個(gè)符號,于是URL 解碼,input 內(nèi)容變成了<\u4e00>,所以對于瀏覽器從服務(wù)器端獲取的頁面數(shù)據(jù)來說,此時(shí)test 對應(yīng)的標(biāo)簽變成了如下:
<a href="javascript:alert('<\u4e00>');">test</a>
這一步完成在所有的工作之前,URL 解碼發(fā)生在第一部,而且它基本上都發(fā)生在服務(wù)器上。
HTML 解析
瀏覽器接收到頁面數(shù)據(jù),于是開始進(jìn)行HTML 解析,構(gòu)造DOM樹。構(gòu)造的過程與語言的編譯過程是相似的,接收文檔,先進(jìn)行詞法分析,然后語法分析,構(gòu)建解析樹。
解析過程是迭代的,解析器從詞法分析器處取道一個(gè)新的符號,并試著用這個(gè)符號匹配一條語法規(guī)則,如果匹配了一條規(guī)則,這個(gè)符號對應(yīng)的節(jié)點(diǎn)將被添加到解析樹上,然后解析器請求另一個(gè)符號。如果沒有匹配到規(guī)則,解析器將在內(nèi)部保存該符號,并從詞法分析器取下一個(gè)符號,直到所有內(nèi)部保存的符號能夠匹配一項(xiàng)語法規(guī)則。如果最終沒有找到匹配的規(guī)則,解析器將拋出一個(gè)異常,這意味著文檔無效或是包含語法錯(cuò)誤。
而最后輸出的樹,也就是這里的解析樹,是由DOM元素及屬性節(jié)點(diǎn)組成的。對于以下一個(gè)最常見的例子:
<html>
<body>
<p>
Hello DOM
</p>
<div><img src=”example.png” /></div>
</body>
</html>
它將轉(zhuǎn)換為下面的DOM 樹:

所以,HTML 的分析器只能識別特定的詞法規(guī)則,才能構(gòu)建起DOM 樹,這一塊,HTML 不會(huì)做解碼的工作,因?yàn)樗霾涣?。所以,試圖這樣構(gòu)造利用漏洞,是不可能的:
<img src="http://www.example.com">
因?yàn)樵跇?gòu)建DOM 樹的時(shí)候,這樣是無法識別的,也就破壞了標(biāo)簽本身的結(jié)構(gòu)。而HTML 解碼,是在DOM樹結(jié)構(gòu)OK,對節(jié)點(diǎn)內(nèi)容解析的時(shí)候,會(huì)進(jìn)行轉(zhuǎn)碼,所以以下兩種寫法,是完全一樣的:

[站外圖片上傳中……(5)]
所以,在DOM 樹構(gòu)建完畢之后,這些HTML 實(shí)體編碼的內(nèi)容就會(huì)被解碼。JS 的解釋器還沒有走進(jìn)戰(zhàn)場。不過由于DOM 的存在,JavaScript還是參與了DOM Tree 的構(gòu)建過程,這時(shí)候,編碼的解析就變得繞了一些。在此我們先忽略掉這一個(gè),先繼續(xù)講主過程講完,繼續(xù)考慮這個(gè)代碼:
<a href="javascript:alert('<\u4e00>');">test</a>
HTMl 解析器構(gòu)建DOM Tree, href中的內(nèi)容,如果識別為實(shí)體編碼的,會(huì)透明的解碼,于是它就變成了這樣:
<a href="javascript:alert('<\u4e00>')">test</a>
CSS 的編碼問題
一般來說,CSS 解析器會(huì)做接下來的工作,不過一般來說,為了考慮到更好的體驗(yàn)和性能,并不會(huì)等到所有的html都解析完成之后再去構(gòu)建和布局render樹。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容,同時(shí),可能還在通過網(wǎng)絡(luò)下載其余內(nèi)容。
當(dāng)然CSS不會(huì)干擾到DOM 樹的建立,他會(huì)結(jié)合CSS文件和style 標(biāo)簽,以及HTML中的課件指令來構(gòu)建起reder tree。這里JavaScrit 的 CSSOM api 也會(huì)出一些力。
CSS 編碼解析是用了一套不太正統(tǒng)的轉(zhuǎn)義策略:用一個(gè)反斜杠,后邊跟1~6位十六進(jìn)制數(shù)字構(gòu)成。,所以字母e 可以編碼為 \65, \065,\000065。而因?yàn)檫@樣,后邊就不能直接緊跟數(shù)字或字母,否則會(huì)被當(dāng)成轉(zhuǎn)義里的內(nèi)容處理,所以CSS 選擇了空格作為終止標(biāo)識,在解碼的時(shí)候,再將空格去除。
同時(shí),CSS還支持直接使用反斜杠對非十六進(jìn)制字符進(jìn)行轉(zhuǎn)義的方式,就按緊跟著反斜杠后邊的字符的字面意思進(jìn)行解釋,這種機(jī)制可用來轉(zhuǎn)義引號和反斜杠本身,不過不能轉(zhuǎn)義HTML 控制的字符,比如尖括號,那是因?yàn)镠TML 解析器總是先于CSS 解析器。
由于CSS 轉(zhuǎn)義規(guī)定的語焉不詳,許多解析器會(huì)對本該用引號括起來的字符串進(jìn)行任意的轉(zhuǎn)義,特別的,在IE 瀏覽器里,這種轉(zhuǎn)義優(yōu)先級高于偽函數(shù)語法,于是下邊兩種情況的寫法是一樣的:
color:expression(alert(1))
color:expression\028 alert \028 1 \029 \029
如果對該部分內(nèi)容感興趣,可以閱讀開始提到的那篇文章,或者是我之前寫的一些文章。
JS 解釋器
上邊提到了style ,是建立reder tree 的時(shí)候使用的,它怎么工作的呢。考慮到我們的瀏覽器為了讓不同的解析器來工作處理不同的內(nèi)容,實(shí)際上,在處理諸如< script> < style> 這樣的標(biāo)簽,解析器會(huì)自動(dòng)切換到特殊解析模式,而src href 后邊加入的JavaScript 偽URL,也會(huì)進(jìn)入JS 的解析模式。而進(jìn)入該解析模式的時(shí)候,該DOM節(jié)點(diǎn)已經(jīng)建立起來了。
還是上邊的例子,經(jīng)過HTML 的解碼,代碼已經(jīng)變成這樣:
<a href="javascript:alert('<\u4e00>')">test</a>
javascript 出發(fā)了JS 解釋器,JS會(huì)先對內(nèi)容進(jìn)行解析,里邊有一個(gè)轉(zhuǎn)義字符\u4e00,前導(dǎo)的 \u 表示他是一個(gè)Unicode 字符,根據(jù)后邊的數(shù)字,解析為'一',于是在完成JS的解析之后變成了:
<a href="javascript:alert('<一>')">test</a>
然后JS 解釋器執(zhí)行alert("< 一>"),這句話會(huì)交給瀏覽器渲染,最終彈窗。
這里邊會(huì)有一個(gè)看起來讓人有些疑惑的東西,仍以上一段代碼為例,假如我們編碼的位置不是括號里,而是在alert上,我們知道,js 是會(huì)對它進(jìn)行逆轉(zhuǎn)義的:
<a href="javascript:\u0061lert('<一>')">test</a>
而另一方面,如果想用這種方式來替換掉圓括號,或者引號,會(huì)判定為失敗。同時(shí),主要注意的是,上邊這種直接在字符串外進(jìn)行轉(zhuǎn)義的方式,只有Unicode 轉(zhuǎn)義方式支持,其他轉(zhuǎn)義方式則不行。其實(shí),這樣的策略是正確的,因?yàn)閷τ贘avaScript,轉(zhuǎn)義編碼應(yīng)當(dāng)只出現(xiàn)在標(biāo)示符部分,不能用于對語法有真正影響的符號,也就是括號,或者是引號。其實(shí),這樣的處理方法,反而是比CSS 更加合理的。
在一個(gè)頁面中,可以出發(fā)JS 解析器的方式有這么幾種:
- 直接嵌入< script> 代碼塊。
- 通過< script sr=... > 加載代碼。
- 各種HTML CSS 參數(shù)支持JavaScript:URL 觸發(fā)調(diào)用。
- CSS expression(...) 語法和某些瀏覽器的XBL 綁定。
- 事件處理器(Event handlers),比如 onload, onerror, onclick等等。
- 定時(shí)器,Timer(setTimeout, setInterval)
- eval(...) 調(diào)用。
我們看到,這些藏匿在HTML 便簽中的各種JS 調(diào)用,就可以想到Web 開發(fā)者的頭有多大了,我們舉一個(gè)簡單的栗子:
比如定時(shí)器那里,考慮以下代碼:
< script>
var value = "user_string";
...
setTimeout("do_stuff('"+value+"')", 1000);
< /script>
表面上看他沒有問題,對 value 只做一次轉(zhuǎn)義就好了,但實(shí)際呢,考慮其解析過程,首先是HTML 解析出script 塊,然后JavaScript 做第一次解析,檢查setTimeout 語法,而等到1秒之后,才會(huì)解析do_stuff,如果不多做一次轉(zhuǎn)義,就有可能構(gòu)造成一次注入,比如user_string 中插入一個(gè)JavaScript編碼的構(gòu)造,截?cái)嗲斑吅瘮?shù),然后構(gòu)造自己的攻擊部分。
下面我們說一說DOM,我們知道常見的DOM 操作:
DOM 常見的方法有:
- 獲取節(jié)點(diǎn)
- getElementsById()
- getElementsByTagName()
- getElementsByClassName()
- 新增結(jié)點(diǎn)
- document.createElement() 創(chuàng)建節(jié)點(diǎn)對象,參數(shù)是字符串也就是html標(biāo)簽
- createTextNode 創(chuàng)建文本節(jié)點(diǎn),配合上一個(gè)使用
- appendChild(element) 把新的結(jié)點(diǎn)添加到指定節(jié)點(diǎn)下,參數(shù)是一個(gè)節(jié)點(diǎn)對象
- insertChild() 在指定結(jié)點(diǎn)錢插入新的節(jié)點(diǎn)
- 修改節(jié)點(diǎn)
- replaceChild() 節(jié)點(diǎn)交換
- setAttribute() 設(shè)置屬性
- 刪除節(jié)點(diǎn)
- removeChild(element) 刪除節(jié)點(diǎn),要先獲得父節(jié)點(diǎn)然后再刪除子節(jié)點(diǎn)
- 一些屬性
- innerHTML 節(jié)點(diǎn)內(nèi)容,可以獲取或者設(shè)置
- parentNode 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)
- childNode 子節(jié)點(diǎn)
- attributes 節(jié)點(diǎn)屬性
- style 修改樣式
這樣就有一些疑惑了,之前,我們說了,基本的解析順序是這樣的,URL 解析器,HTML 解析器, CSS 解析器,JS解析器,如果安安靜靜的按照這個(gè)順序下去,應(yīng)該很容易理清楚。然而DOM 操作里我們可以看到可以新增節(jié)點(diǎn),也可以修改節(jié)點(diǎn)屬性,節(jié)點(diǎn)的內(nèi)容和樣式都可以修改。那么假如我們使用innerHTML 修改了某節(jié)點(diǎn)的內(nèi)容,讓其構(gòu)成了一個(gè)新的節(jié)點(diǎn),那么會(huì)有什么效果呢?我在Chrome 上做了一些實(shí)驗(yàn),代碼如下:
<html>
<body>
<p id="1">hello</p>
<img src=# onerror="alert(1)" />
<script>
document.getElementById("1").innerHTML = "<img src=# on\u0065rror=alert(1)>";
</script>
<!--<script>
document.getElementById("1").innerHTML = "<img src="1" onerror="alert(1)">";
&#97;
</script>-->
</body>
<html>
一個(gè)正常的容易理解的過程是這一行:
<img src=# onerror="alert(1)" />
HTML 解析到標(biāo)簽,建立DOM 樹,然后對節(jié)點(diǎn)內(nèi)容進(jìn)行實(shí)體解碼,a; 就變成a, 隨后在js 解析階段,正常的觸發(fā)了彈窗,先后順序OK。
但對于下面這段代碼:
<script>
document.getElementById("1").innerHTML = "<img src=# on\u0065rror=alert(1)>";
</script>
使用了DOM 操作,修改前邊標(biāo)簽中的內(nèi)容,添加了一個(gè)img 內(nèi)容,因?yàn)檫M(jìn)入了script 進(jìn)入了JavaScript的特殊解析模式,所以此處HTML 不得干擾,首先JavaScript解析器,會(huì)先對其中編碼的內(nèi)容解碼,于是onerror 就還原回來了,于是正常的執(zhí)行了JS 語句,在HTML 文檔中,將hello 變成了img。
那么問題來了,如上那樣,對onerror 的內(nèi)容作了HTML 實(shí)體編碼,會(huì)不會(huì)彈窗呢? 答案是顯然的,該標(biāo)簽傳回給HTML,HTML 建立DOM節(jié)點(diǎn),透明的解碼節(jié)點(diǎn)內(nèi)容,onerror 又會(huì)執(zhí)行其中的JS 腳本,彈出窗口。
其實(shí),這里也不難理解,因?yàn)镠TML 是從上到下解析,遇到< script> 于是進(jìn)入了特殊的解析模式,使用JS 解析器,做了一個(gè)DOM 操作,該DOM 操作修改了前邊的DOM 樹,該塊內(nèi)容,需要使用HTML 解析重塑DOM 樹,那么節(jié)點(diǎn)內(nèi)容中的實(shí)體編碼就會(huì)被解碼,然后onerror 中觸發(fā)腳本,JS 又會(huì)對內(nèi)容進(jìn)行一次解析。
這一點(diǎn)很好理解:
[站外圖片上傳中……(6)]
如上,內(nèi)容中有HTML實(shí)體編碼,還有js 的Unicode 編碼,正常彈窗沒有問題。
總結(jié)說來,實(shí)際上,DOM 操作實(shí)際上是js強(qiáng)勢介入 HTML 和CSS 的結(jié)果,使用DOM 操作,對DOM Tree 造成了改變,會(huì)調(diào)用到HTML 解析器重新對其解析,于是流程又會(huì)返回到最開始說的那個(gè)解析流程里去。這樣反復(fù)的情況,再加上編碼的重疊,很容易讓開發(fā)者無所適從,考慮下面的代碼:
<p id="1">hello</p>
[站外圖片上傳中……(7)]
<script>
document.getElementById("1").innerHTML = "bye";
function a(){
document.getElementById("1").innerHTML = "<img src=# on\u0065rror=alert(1)>";
}
function timedMsg()
{
var t=setTimeout("a()",5000)
}
</script>
<input type="button" value="111" onClick = "timedMsg()" />
整個(gè)頁面渲染完畢,而當(dāng)點(diǎn)擊按鈕之后,會(huì)觸發(fā)DOM操作的腳本,五秒鐘之后,彈窗。
如果想要修改的內(nèi)容中有腳本,內(nèi)容中的腳本部分使用JS編碼,再使用HTML 編碼,然后再使用JS Unicode編碼,那么解碼的過程就是先JS 解碼,再HTML 解碼,再JS 解碼,然后執(zhí)行。
總結(jié)
其實(shí)總的來說,其道理還是易于理解的,只是由于一些特別的操作,造成了一些困擾,于是在編碼上,會(huì)理不清頭緒,如果再此基礎(chǔ)上我們再使用String.fromCharCode() 這個(gè)一直以來容易被開發(fā)者忽略的功能,更會(huì)摸不清頭腦。
而正是由于這種摸不清頭腦的開發(fā)之下,黑客們才有可趁之機(jī),制造各種變體,繞過孱弱的過濾器。
總體來說,在編碼這件事上,只要理清楚,URL解碼,HTML解碼,CSS解碼,JS解碼,以及DOM 操作在其中扮演的角色,就基本上能理清楚了。作為一個(gè)開發(fā)者,安全編碼是必須要重視的內(nèi)容,所以,對于編碼,不可以逃避,在構(gòu)建安全的過濾規(guī)則的時(shí)候,一定要考慮清楚各種可能的編碼繞過的方式,以避免損失。
如何避免這些漏洞的產(chǎn)生,我會(huì)再以后繼續(xù)寫,關(guān)于瀏覽器的解碼過程就寫到這里吧。對這一問題,仍然還有一些問題有待解決,個(gè)人能力有限,其中也有可能存在錯(cuò)誤和疏漏,請諒解~
PS. 參考:開頭提到的文章,W3cschool 上各種函數(shù),各種編碼?!禩he Tangled Web》這本神書,和一堆谷歌搜索。
pps. 因?yàn)榻曆?,打字時(shí)候,不怎么喜歡盯著屏幕,所以。。。。錯(cuò)別字可能有點(diǎn)多。