轉自:http://www.cnblogs.com/coco1s/p/5777260.html
作為前端,一直以來都知道
HTTP劫持
與 XSS跨站腳本
(Cross-site scripting)、 CSRF跨站請求僞造
(Cross-site request forgery)。但是一直都沒有深入研究過,前些日子同僚的分享會偶然提及,我也對這一塊很感興趣,便深入研究了一番。
最近用 JavaScript 寫了一個元件,可以在前端層面防禦部分 HTTP 劫持與 XSS。
當然,防禦這些劫持最好的方法還是從後端入手,前端能做的實在太少。而且由于源碼的暴露,攻擊者很容易繞過我們的防禦手段。但是這不代表我們去了解這塊的相關知識是沒意義的,本文的許多方法,用在其他方面也是大有作用。
已上傳到 Github – httphijack.js ,歡迎感興趣看看順手點個 star ,本文示例代碼,防範方法在元件源碼中皆可找到。
接下來進入正文。
HTTP劫持、DNS劫持與XSS
先簡單講講什麼是 HTTP 劫持與 DNS 劫持。
HTTP劫持
什麼是HTTP劫持呢,大多數情況是營運商HTTP劫持,當我們使用HTTP請求請求一個網站頁面的時候,網絡營運商會在正常的資料流中插入精心設計的網絡資料封包,讓用戶端(通常是浏覽器)展示“錯誤”的資料,通常是一些彈窗,宣傳性廣告或者直接顯示某網站的内容,大家應該都有遇到過。
DNS劫持
DNS劫持就是通過劫持了DNS伺服器,通過某些手段取得某域名的解析記錄控制權,進而修改此域名的解析結果,導緻對該域名的通路由原IP位址轉入到修改後的指定IP,其結果就是對特定的網址不能通路或通路的是假網址,進而實作竊取資料或者破壞原有正常服務的目的。
DNS 劫持就更過分了,簡單說就是我們請求的是 http://www.a.com/index.html ,直接被重定向了 http://www.b.com/index.html ,本文不會過多讨論這種情況。
XSS跨站腳本
XSS指的是攻擊者漏洞,向 Web 頁面中注入惡意代碼,當使用者浏覽該頁之時,注入的代碼會被執行,進而達到攻擊的特殊目的。
關于這些攻擊如何生成,攻擊者如何注入惡意代碼到頁面中本文不做讨論,隻要知道如 HTTP 劫持 和 XSS 最終都是惡意代碼在用戶端,通常也就是使用者浏覽器端執行,本文将讨論的就是假設注入已經存在,如何利用 Javascript 進行行之有效的前端防護。
頁面被嵌入 iframe 中,重定向 iframe
先來說說我們的頁面被嵌入了 iframe 的情況。也就是,網絡營運商為了盡可能地減少植入廣告對原有網站頁面的影響,通常會通過把原有網站頁面放置到一個和原頁面相同大小的 iframe 裡面去,那麼就可以通過這個 iframe 來隔離廣告代碼對原有頁面的影響。

這種情況還比較好處理,我們隻需要知道我們的頁面是否被嵌套在 iframe 中,如果是,則重定向外層頁面到我們的正常頁面即可。
那麼有沒有方法知道我們的頁面目前存在于 iframe 中呢?有的,就是
window.self
與
window.top
。
window.self
傳回一個指向目前 window 對象的引用。
window.top
傳回視窗體系中的最頂層視窗的引用。
對于非同源的域名,iframe 子頁面無法通過 parent.location 或者 top.location 拿到具體的頁面位址,但是可以寫入 top.location ,也就是可以控制父頁面的跳轉。
兩個屬性分别可以又簡寫為
self
top
,是以當發現我們的頁面被嵌套在 iframe 時,可以重定向父級頁面:
1 2 3 4 5 6 | |
使用白名單放行正常 iframe 嵌套
當然很多時候,也許營運需要,我們的頁面會被以各種方式推廣,也有可能是正常業務需要被嵌套在 iframe 中,這個時候我們需要一個白名單或者黑名單,當我們的頁面被嵌套在 iframe 中且父級頁面域名存在白名單中,則不做重定向操作。
上面也說了,使用 top.location.href 是沒辦法拿到父級頁面的 URL 的,這時候,需要使用
document.referrer
。
通過 document.referrer 可以拿到跨域 iframe 父頁面的URL。
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
更改 URL 參數繞過營運商标記
這樣就完了嗎?沒有,我們雖然重定向了父頁面,但是在重定向的過程中,既然第一次可以嵌套,那麼這一次重定向的過程中頁面也許又被 iframe 嵌套了,真尼瑪蛋疼。
當然營運商這種劫持通常也是有迹可循,最正常的手段是在頁面 URL 中設定一個參數,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中
iframe_hijack_redirected=1
表示頁面已經被劫持過了,就不再嵌套 iframe 了。是以根據這個特性,我們可以改寫我們的 URL ,使之看上去已經被劫持了:
29 30 31 32 | |
當然,如果這個參數一改,防嵌套的代碼就失效了。是以我們還需要建立一個上報系統,當發現頁面被嵌套時,發送一個攔截上報,即便重定向失敗,也可以知道頁面嵌入 iframe 中的 URL,根據分析這些 URL ,不斷增強我們的防護手段,這個後文會提及。
内聯事件及内聯腳本攔截
在 XSS 中,其實可以注入腳本的方式非常的多,尤其是 HTML5 出來之後,一不留神,許多的新标簽都可以用于注入可執行腳本。
列出一些比較常見的注入方式:
-
<a href="javascript:alert(1)" ></a>
-
<iframe src="javascript:alert(1)" />
-
<img src='x' onerror="alert(1)" />
-
<video src='x' onerror="alert(1)" ></video>
-
<div onclick="alert(1)" onmouseover="alert(2)" ><div>
除去一些未列出來的非常少見生僻的注入方式,大部分都是
javascript:...
及内聯事件
on*
我們假設注入已經發生,那麼有沒有辦法攔截這些内聯事件與内聯腳本的執行呢?
對于上面列出的 (1) (5) ,這種需要使用者點選或者執行某種事件之後才執行的腳本,我們是有辦法進行防禦的。
浏覽器事件模型
這裡說能夠攔截,涉及到了
事件模型
相關的原理。
我們都知道,标準浏覽器事件模型存在三個階段:
- 捕獲階段
- 目标階段
- 冒泡階段
對于一個這樣
<a href="javascript:alert(222)" ></a>
的 a 标簽而言,真正觸發元素
alert(222)
是處于點選事件的目标階段。
點選上面的
click me
,先彈出 111 ,後彈出 222。
那麼,我們隻需要在點選事件模型的捕獲階段對标簽内
javascript:...
的内容建立關鍵字黑名單,進行過濾審查,就可以做到我們想要的攔截效果。
對于 on* 類内聯事件也是同理,隻是對于這類事件太多,我們沒辦法手動枚舉,可以利用代碼自動枚舉,完成對内聯事件及内聯腳本的攔截。
以攔截 a 标簽内的
href="javascript:...
為例,我們可以這樣寫:
33 34 35 36 37 38 39 40 41 42 43 | |
可以戳我檢視DEMO。(打開頁面後打開控制台檢視 console.log)
點選圖中這幾個按鈕,可以看到如下:
這裡我們用到了黑名單比對,下文還會細說。
靜态腳本攔截
XSS 跨站腳本的精髓不在于“跨站”,在于“腳本”。
通常而言,攻擊者或者營運商會向頁面中注入一個
<script>
腳本,具體操作都在腳本中實作,這種劫持方式隻需要注入一次,有改動的話不需要每次都重新注入。
我們假定現在頁面上被注入了一個
<script src="http://attack.com/xss.js">
腳本,我們的目标就是攔截這個腳本的執行。
聽起來很困難啊,什麼意思呢。就是在腳本執行前發現這個可疑腳本,并且銷毀它使之不能執行内部代碼。
是以我們需要用到一些進階 API ,能夠在頁面加載時對生成的節點進行檢測。
MutationObserver
MutationObserver 是 HTML5 新增的 API,功能很強大,給開發者們提供了一種能在某個範圍内的 DOM 樹發生變化時作出适當反應的能力。
說的很玄乎,大概的意思就是能夠監測到頁面 DOM 樹的變換,并作出反應。
MutationObserver()
該構造函數用來執行個體化一個新的Mutation觀察者對象。
|
目瞪狗呆,這一大段又是啥?意思就是 MutationObserver 在觀測時并非發現一個新元素就立即回調,而是将一個時間片段裡出現的所有元素,一起傳過來。是以在回調中我們需要進行批量處理。而且,其中的
callback
會在指定的 DOM 節點(目标節點)發生變化時被調用。在調用時,觀察者對象會傳給該函數兩個參數,第一個參數是個包含了若幹個 MutationRecord 對象的數組,第二個參數則是這個觀察者對象本身。
是以,使用 MutationObserver ,我們可以對頁面加載的每個靜态腳本檔案,進行監控:
|
可以看到如下:可以戳我檢視DEMO。(打開頁面後打開控制台檢視 console.log)
<script type="text/javascript" src="./xss/a.js"></script>
是頁面加載一開始就存在的靜态腳本(檢視頁面結構),我們使用 MutationObserver 可以在腳本加載之後,執行之前這個時間段對其内容做正則比對,發現惡意代碼則
removeChild()
掉,使之無法執行。
使用白名單對 src 進行比對過濾
上面的代碼中,我們判斷一個js腳本是否是惡意的,用的是這一句:
|
當然實際當中,注入惡意代碼者不會那麼傻,把名字改成 XSS 。是以,我們很有必要使用白名單進行過濾和建立一個攔截上報系統。
|
這裡我們已經多次提到白名單比對了,下文還會用到,是以可以這裡把它簡單封裝成一個方法調用。
動态腳本攔截
上面使用 MutationObserver 攔截靜态腳本,除了靜态腳本,與之對應的就是動态生成的腳本。
|
要攔截這類動态生成的腳本,且攔截時機要在它插入 DOM 樹中,執行之前,本來是可以監聽
Mutation Events
中的
DOMNodeInserted
事件的。
Mutation Events 與 DOMNodeInserted
打開 MDN ,第一句就是:
該特性已經從 Web 标準中删除,雖然一些浏覽器目前仍然支援它,但也許會在未來的某個時間停止支援,請盡量不要使用該特性。
雖然不能用,也可以了解一下:
|
然而可惜的是,使用上面的代碼攔截動态生成的腳本,可以攔截到,但是代碼也執行了:
DOMNodeInserted
顧名思義,可以監聽某個 DOM 範圍内的結構變化,與
MutationObserver
相比,它的執行時機更早。
但是
DOMNodeInserted
不再建議使用,是以監聽動态腳本的任務也要交給
MutationObserver
可惜的是,在實際實踐過程中,使用
MutationObserver
的結果和
DOMNodeInserted
一樣,可以監聽攔截到動态腳本的生成,但是無法在腳本執行之前,使用
removeChild
将其移除,是以我們還需要想想其他辦法。
重寫 setAttribute 與 document.write
重寫原生 Element.prototype.setAttribute 方法
在動态腳本插入執行前,監聽 DOM 樹的變化攔截它行不通,腳本仍然會執行。
那麼我們需要向上尋找,在腳本插入 DOM 樹前的捕獲它,那就是建立腳本時這個時機。
假設現在有一個動态腳本是這樣建立的:
|
而重寫
Element.prototype.setAttribute
也是可行的:我們發現這裡用到了 setAttribute 方法,如果我們能夠改寫這個原生方法,監聽設定
src
屬性時的值,通過黑名單或者白名單判斷它,就可以判斷該标簽的合法性了。
44 45 46 | |
可以看到如下結果:可以戳我檢視DEMO。(打開頁面後打開控制台檢視 console.log)
重寫
Element.prototype.setAttribute
,就是首先儲存原有接口,然後當有元素調用 setAttribute 時,檢查傳入的 src 是否存在于白名單中,存在則放行,不存在則視為可疑元素,進行上報并不予以執行。最後對放行的元素執行原生的
setAttribute
,也就是
old_setAttribute.apply(this, arguments);
上述的白名單比對也可以換成黑名單比對。
重寫嵌套 iframe 内的 Element.prototype.setAttribute
當然,上面的寫法如果
old_setAttribute = Element.prototype.setAttribute
暴露給攻擊者的話,直接使用
old_setAttribute
就可以繞過我們重寫的方法了,是以這段代碼必須包在一個閉包内。
當然這樣也不保險,雖然目前視窗下的
Element.prototype.setAttribute
已經被重寫了。但是還是有手段可以拿到原生的
Element.prototype.setAttribute
,隻需要一個新的 iframe 。
|
通過這個方法,可以重新拿到原生的
Element.prototype.setAttribute
,因為 iframe 内的環境和外層 window 是完全隔離的。wtf?
怎麼辦?我們看到建立 iframe 用到了
createElement
,那麼是否可以重寫原生
createElement
呢?但是除了
createElement
還有
createElementNS
,還有可能是頁面上已經存在 iframe,是以不合适。
那就在每當新建立一個新 iframe 時,對
setAttribute
進行保護重寫,這裡又有用到
MutationObserver
:
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
我們定義了一個
installHook
方法,參數是一個
window
,在這個方法裡,我們将重寫傳入的
window
下的 setAttribute ,并且安裝一個
MutationObserver
,并對此視窗下未來可能建立的
iframe
進行監聽,如果未來在此
window
下建立了一個 iframe ,則對新的
iframe
也裝上
installHook
方法,以此進行層層保護。
重寫 document.write
根據上述的方法,我們可以繼續挖掘一下,還有什麼方法可以重寫,以便對頁面進行更好的保護。
document.write
是一個很不錯選擇,注入攻擊者,通常會使用這個方法,往頁面上注入一些彈窗廣告。
我們可以重寫
document.write
,使用關鍵詞黑名單對内容進行比對。
什麼比較适合當黑名單的關鍵字呢?我們可以看看一些廣告很多的頁面:
這裡在頁面最底部嵌入了一個 iframe ,裡面裝了廣告代碼,這裡的最外層的 id 名
id="BAIDU_SSP__wrapper_u2444091_0"
就很适合成為我們判斷是否是惡意代碼的一個标志,假設我們已經根據攔截上報收集到了一批黑名單清單:
|
接下來我們隻需要利用這些關鍵字,對
document.write
傳入的内容進行正則判斷,就能确定是否要攔截
document.write
這段代碼。
|
我們可以把
resetDocumentWrite
放入上文的
installHook
方法中,就能對目前 window 及所有生成的 iframe 環境内的
document.write
進行重寫了。
鎖死 apply 和 call
接下來要介紹的這個是鎖住原生的 Function.prototype.apply 和 Function.prototype.call 方法,鎖住的意思就是使之無法被重寫。
這裡要用到
Object.defineProperty
,用于鎖死 apply 和 call。
Object.defineProperty
Object.defineProperty() 方法直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 并傳回這個對象。
|
其中:
- obj – 需要定義屬性的對象
- prop – 需被定義或修改的屬性名
- descriptor – 需被定義或修改的屬性的描述符
我們可以使用如下的代碼,讓 call 和 apply 無法被重寫。
|
為啥要這樣寫呢?其實還是與上文的
重寫 setAttribute
有關。
雖然我們将原始 Element.prototype.setAttribute 儲存在了一個閉包當中,但是還有奇技淫巧可以把它從閉包中給“偷出來”。
試一下:
|
猜猜上面一段會輸出什麼?看看:
居然傳回了原生 setAttribute 方法!
這是因為我們在重寫
Element.prototype.setAttribute
時最後有
old_setAttribute.apply(this, arguments);
這一句,使用到了 apply 方法,是以我們再重寫
apply
,輸出
this
,當調用被重寫後的 setAttribute 就可以從中反向拿到原生的被儲存起來的
old_setAttribute
了。
這樣我們上面所做的嵌套 iframe 重寫 setAttribute 就毫無意義了。
使用上面的
Object.defineProperty
可以鎖死 apply 和 類似用法的 call 。使之無法被重寫,那麼也就無法從閉包中将我們的原生接口偷出來。這個時候才算真正意義上的成功重寫了我們想重寫的屬性。
建立攔截上報
防禦的手段也有一些了,接下來我們要建立一個上報系統,替換上文中的 console.log() 日志。
上報系統有什麼用呢?因為我們用到了白名單,關鍵字黑名單,這些資料都需要不斷的豐富,靠的就是上報系統,将每次攔截的資訊傳到伺服器,不僅可以讓我們程式員第一時間得知攻擊的發生,更可以讓我們不斷收集這類相關資訊以便更好的應對。
這裡的示例我用
nodejs
搭一個十分簡易的伺服器接受 http 上報請求。
先定義一個上報函數:
|
假定我們的伺服器位址是
www.reportServer.com
這裡,我們運用
img.src
發送一個 http 請求到伺服器
http://www.reportServer.com/report/
,每次會帶上我們自定義的攔截類型,攔截内容以及上報時間。
用 Express 搭 nodejs 伺服器并寫一個簡單的接收路由:
|
運作伺服器,當有上報發生,我們将會接收到如下資料:
好接下來就是資料入庫,分析,添加黑名單,使用
nodejs
當然攔截發生時發送郵件通知程式員等等,這些就不再做展開。
HTTPS 與 CSP
最後再簡單談談 HTTPS 與 CSP。其實防禦劫持最好的方法還是從後端入手,前端能做的實在太少。而且由于源碼的暴露,攻擊者很容易繞過我們的防禦手段。
CSP
CSP 即是 Content Security Policy,翻譯為内容安全政策。這個規範與内容安全有關,主要是用來定義頁面可以加載哪些資源,減少 XSS 的發生。
MDN – CSP
HTTPS
能夠實施 HTTP 劫持的根本原因,是 HTTP 協定沒有辦法對通信對方的身份進行校驗以及對資料完整性進行校驗。如果能解決這個問題,則劫持将無法輕易發生。
HTTPS,是 HTTP over SSL 的意思。SSL 協定是 Netscape 在 1995 年首次提出的用于解決傳輸層安全問題的網絡協定,其核心是基于公鑰密碼學理論實作了對伺服器身份認證、資料的私密性保護以及對資料完整性的校驗等功能。
因為與本文主要内容關聯性不大,關于更多 CSP 和 HTTPS 的内容可以自行谷歌。
本文到此結束,我也是涉獵前端安全這個方面不久,文章必然有所纰漏及錯誤,文章的方法也是衆多防禦方法中的一小部分,許多内容參考下面文章,都是精品文章,非常值得一讀:
- 《web前端黑客技術揭秘》
- XSS 前端防火牆系列1~3
- 【HTTP劫持和DNS劫持】實際JS對抗
- 淺談DNS劫持
- HTTP Request Hijacking
使用 Javascript 寫的一個防劫持元件,已上傳到 Github – httphijack.js,歡迎感興趣看看順手點個 star ,本文示例代碼,防範方法在元件源碼中皆可找到。
另外元件處于測試修改階段,未在生産環境使用,而且使用了很多 HTML5 才支援的 API,相容性是個問題,僅供學習交流。
到此本文結束,如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。
分類: Javascript, 前端安全