防御XSS攻擊的七條原則
本文將會(huì)著重介紹防御XSS攻擊的一些原則,需要讀者對(duì)于XSS有所了解,至少知道XSS漏洞的基本原理,如果您對(duì)此不是特別清楚,請(qǐng)參考這兩篇文章:《Stored and Reflected XSS Attack》《DOM Based XSS》
攻擊者可以利用XSS漏洞向用戶發(fā)送攻擊腳本,而用戶的瀏覽器因?yàn)闆](méi)有辦法知道這段腳本是不可信的,所以依然會(huì)執(zhí)行它。對(duì)于瀏覽器而言,它認(rèn)為這段腳本是來(lái)自可以信任的服務(wù)器的,所以腳本可以光明正大地訪問(wèn)Cookie,或者保存在瀏覽器里被當(dāng)前網(wǎng)站所用的敏感信息,甚至可以知道用戶電腦安裝了哪些軟件。這些腳本還可以改寫(xiě)HTML頁(yè)面,進(jìn)行釣魚(yú)攻擊。
雖然產(chǎn)生XSS漏洞的原因各種各樣,對(duì)于漏洞的利用也是花樣百出,但是如果我們遵循本文提到防御原則,我們依然可以做到防止XSS攻擊的發(fā)生。
有人可能會(huì)問(wèn),防御XSS的核心不就是在輸出不可信數(shù)據(jù)的時(shí)候進(jìn)行編碼,而現(xiàn)如今流行的Web框架(比如Rails)大多都在默認(rèn)情況下就對(duì)不可信數(shù)據(jù)進(jìn)行了HTML編碼,幫我們做了防御,還用得著我們自己再花時(shí)間研究如何防御XSS嗎?答案是肯定的,對(duì)于將要放置到HTML頁(yè)面body里的不可信數(shù)據(jù),進(jìn)行HTML編碼已經(jīng)足夠防御XSS攻擊了,甚至將HTML編碼后的數(shù)據(jù)放到HTML標(biāo)簽(TAG)的屬性(attribute)里也不會(huì)產(chǎn)生XSS漏洞(但前提是這些屬性都正確使用了引號(hào)),但是,如果你將HTML編碼后的數(shù)據(jù)放到了。
<script>…不要在這里直接插入不可信數(shù)據(jù)…</script>直接插入到SCRIPT標(biāo)簽里 <!– …不要在這里直接插入不可信數(shù)據(jù)… –> 插入到HTML注釋里 <div 不要在這里直接插入不可信數(shù)據(jù)=”…”></div> 插入到HTML標(biāo)簽的屬性名里 <div name=”…不要在這里直接插入不可信數(shù)據(jù)…”></div> 插入到HTML標(biāo)簽的屬性值里 <不要在這里直接插入不可信數(shù)據(jù) href=”…”></a> 作為HTML標(biāo)簽的名字 <style>…不要在這里直接插入不可信數(shù)據(jù)…</style> 直接插入到CSS里
最重要的是,千萬(wàn)不要引入任何不可信的第三方JavaScript到頁(yè)面里,一旦引入了,這些腳本就能夠操縱你的HTML頁(yè)面,竊取敏感信息或者發(fā)起釣魚(yú)攻擊等等。
原則2:在將不可信數(shù)據(jù)插入到HTML標(biāo)簽之間時(shí),對(duì)這些數(shù)據(jù)進(jìn)行HTML Entity編碼
在這里相當(dāng)強(qiáng)調(diào)是往HTML標(biāo)簽之間插入不可信數(shù)據(jù),以區(qū)別于往HTML標(biāo)簽屬性部分插入不可信數(shù)據(jù),因?yàn)檫@兩者需要進(jìn)行不同類(lèi)型的編碼。當(dāng)你確實(shí)需要往HTML標(biāo)簽之間插入不可信數(shù)據(jù)的時(shí)候,首先要做的就是對(duì)不可信數(shù)據(jù)進(jìn)行HTML Entity編碼。比如,我們經(jīng)常需要往DIV,P,TD這些標(biāo)簽里放入一些用戶提交的數(shù)據(jù),這些數(shù)據(jù)是不可信的,需要對(duì)它們進(jìn)行HTML Entity編碼。很多Web框架都提供了HTML Entity編碼的函數(shù),我們只需要調(diào)用這些函數(shù)就好,而有些Web框架似乎更“智能”,比如Rails,它能在默認(rèn)情況下對(duì)所有插入到HTML頁(yè)面的數(shù)據(jù)進(jìn)行HTML Entity編碼,盡管不能完全防御XSS,但著實(shí)減輕了開(kāi)發(fā)人員的負(fù)擔(dān)。
<body>…插入不可信數(shù)據(jù)前,對(duì)其進(jìn)行HTML Entity編碼…</body> <div>…插入不可信數(shù)據(jù)前,對(duì)其進(jìn)行HTML Entity編碼…</div> <p>…插入不可信數(shù)據(jù)前,對(duì)其進(jìn)行HTML Entity編碼…</p> 以此類(lèi)推,往其他HTML標(biāo)簽之間插入不可信數(shù)據(jù)前,對(duì)其進(jìn)行HTML Entity編碼
[編碼規(guī)則]
那么HTML Entity編碼具體應(yīng)該做哪些事情呢?它需要對(duì)下面這6個(gè)特殊字符進(jìn)行編碼:
& –> &
< –> <
> –> >
” –> "
‘ –> '
/ –> /
有兩點(diǎn)需要特別說(shuō)明的是:
不推薦將單引號(hào)( ‘ )編碼為 ' 因?yàn)樗⒉皇菢?biāo)準(zhǔn)的HTML標(biāo)簽
需要對(duì)斜杠號(hào)( / )編碼,因?yàn)樵谶M(jìn)行XSS攻擊時(shí),斜杠號(hào)對(duì)于關(guān)閉當(dāng)前HTML標(biāo)簽非常有用
推薦使用OWASP提供的ESAPI函數(shù)庫(kù),它提供了一系列非常嚴(yán)格的用于進(jìn)行各種安全編碼的函數(shù)。在當(dāng)前這個(gè)例子里,你可以使用:
String encodedContent =
ESAPI.encoder().encodeForHTML(request.getParameter(“input”));
原則3:在將不可信數(shù)據(jù)插入到HTML屬性里時(shí),對(duì)這些數(shù)據(jù)進(jìn)行HTML屬性編碼
這條原則是指,當(dāng)你要往HTML屬性(例如width、name、value屬性)的值部分(data value)插入不可信數(shù)據(jù)的時(shí)候,應(yīng)該對(duì)數(shù)據(jù)進(jìn)行HTML屬性編碼。不過(guò)需要注意的是,當(dāng)要往HTML標(biāo)簽的事件處理屬性(例如onmouseover)里插入數(shù)據(jù)的時(shí)候,本條原則不適用,應(yīng)該用下面介紹的原則4對(duì)其進(jìn)行JavaScript編碼。
<div attr=…插入不可信數(shù)據(jù)前,進(jìn)行HTML屬性編碼…></div>屬性值部分沒(méi)有使用引號(hào),不推薦 <div attr=’…插入不可信數(shù)據(jù)前,進(jìn)行HTML屬性編碼…’></div> 屬性值部分使用了單引號(hào) <div attr=”…插入不可信數(shù)據(jù)前,進(jìn)行HTML屬性編碼…”></div> 屬性值部分使用了雙引號(hào)
[編碼規(guī)則]
除了阿拉伯?dāng)?shù)字和字母,對(duì)其他所有的字符進(jìn)行編碼,只要該字符的ASCII碼小于256。編碼后輸出的格式為 &#xHH; (以&#x開(kāi)頭,HH則是指該字符對(duì)應(yīng)的十六進(jìn)制數(shù)字,分號(hào)作為結(jié)束符)
之所以編碼規(guī)則如此嚴(yán)格,是因?yàn)殚_(kāi)發(fā)者有時(shí)會(huì)忘記給屬性的值部分加上引號(hào)。如果屬性值部分沒(méi)有使用引號(hào)的話,攻擊者很容易就能閉合掉當(dāng)前屬性,隨后即可插入攻擊腳本。例如,如果屬性沒(méi)有使用引號(hào),又沒(méi)有對(duì)數(shù)據(jù)進(jìn)行嚴(yán)格編碼,那么一個(gè)空格符就可以閉合掉當(dāng)前屬性。請(qǐng)看下面這個(gè)攻擊:
假設(shè)HTML代碼是這樣的:
攻擊者可以構(gòu)造這樣的輸入:
x onmouseover=”javascript:alert(/xss/)”
最后,在用戶的瀏覽器里的最終HTML代碼會(huì)變成這個(gè)樣子:
只要用戶的鼠標(biāo)移動(dòng)到這個(gè)DIV上,就會(huì)觸發(fā)攻擊者寫(xiě)好的攻擊腳本。在這個(gè)例子里,腳本僅僅彈出一個(gè)警告框,除了惡作劇一下也沒(méi)有太多的危害,但是在真實(shí)的攻擊中,攻擊者會(huì)使用更加具有破壞力的腳本,例如下面這個(gè)竊取用戶cookie的XSS攻擊:
x />
除了空格符可以閉合當(dāng)前屬性外,這些符號(hào)也可以:
% * + , – / ; < = > ^ | `(反單引號(hào),IE會(huì)認(rèn)為它是單引號(hào))
可以使用ESAPI提供的函數(shù)進(jìn)行HTML屬性編碼:
String encodedContent = ESAPI.encoder().encodeForHTMLAttribute
(request.getParameter(“input”));
原則4:在將不可信數(shù)據(jù)插入到SCRIPT里時(shí),對(duì)這些數(shù)據(jù)進(jìn)行SCRIPT編碼
這條原則主要針對(duì)動(dòng)態(tài)生成的JavaScript代碼,這包括腳本部分以及HTML標(biāo)簽的事件處理屬性(Event Handler,如onmouseover, onload等)。在往JavaScript代碼里插入數(shù)據(jù)的時(shí)候,只有一種情況是安全的,那就是對(duì)不可信數(shù)據(jù)進(jìn)行JavaScript編碼,并且只把這些數(shù)據(jù)放到使用引號(hào)包圍起來(lái)的值部分(data value)之中,例如:
除此之外,往JavaScript代碼里其他任何地方插入不可信數(shù)據(jù)都是相當(dāng)危險(xiǎn)的,攻擊者可以很容易地插入攻擊代碼。
<script>alert(‘…插入不可信數(shù)據(jù)前,進(jìn)行JavaScript編碼…’)</script> 值部分使用了單引號(hào) <script>x = “…插入不可信數(shù)據(jù)前,進(jìn)行JavaScript編碼…”</script> 值部分使用了雙引號(hào) <div onmouseover=”x=’…插入不可信數(shù)據(jù)前,進(jìn)行JavaScript編碼…’“</div> 值部分使用了引號(hào),且事件處理屬性的值部分也使用了引號(hào) 特別需要注意的是,在XSS防御中,有些JavaScript函數(shù)是極度危險(xiǎn)的, 就算對(duì)不可信數(shù)據(jù)進(jìn)行JavaScript編碼,也依然會(huì)產(chǎn)生XSS漏洞,例如: <script> window.setInterval(‘…就算對(duì)不可信數(shù)據(jù)進(jìn)行了JavaScript編碼, 這里依然會(huì)有XSS漏洞…’);</script>
[編碼規(guī)則]
除了阿拉伯?dāng)?shù)字和字母,對(duì)其他所有的字符進(jìn)行編碼,只要該字符的ASCII碼小于256。編碼后輸出的格式為 \xHH (以 \x 開(kāi)頭,HH則是指該字符對(duì)應(yīng)的十六進(jìn)制數(shù)字)
在對(duì)不可信數(shù)據(jù)做編碼的時(shí)候,千萬(wàn)不能圖方便使用反斜杠( \ )對(duì)特殊字符進(jìn)行簡(jiǎn)單轉(zhuǎn)義,比如將雙引號(hào) ” 轉(zhuǎn)義成 \” ,這樣做是不可靠的,因?yàn)闉g覽器在對(duì)頁(yè)面做解析的時(shí)候,會(huì)先進(jìn)行HTML解析,然后才是JavaScript解析,所以雙引號(hào)很可能會(huì)被當(dāng)做HTML字符進(jìn)行HTML解析,這時(shí)雙引號(hào)就可以突破代碼的值部分,使得攻擊者可以繼續(xù)進(jìn)行XSS攻擊。例如:
假設(shè)代碼片段如下:
<script>var message = ” $VAR “;</script>
攻擊者輸入的內(nèi)容為:
\”; alert(‘xss’);//
如果只是對(duì)雙引號(hào)進(jìn)行簡(jiǎn)單轉(zhuǎn)義,將其替換成 \” 的話,攻擊者輸入的內(nèi)容在最終的頁(yè)面上會(huì)變成:
<script>var message = ” \\”; alert(‘xss’);// “;</script>
瀏覽器在解析的時(shí)候,會(huì)認(rèn)為反斜杠后面的那個(gè)雙引號(hào)和第一個(gè)雙引號(hào)相匹配,繼而認(rèn)為后續(xù)的alert(‘xss’)是正常的JavaScript腳本,因此允許執(zhí)行。
可以使用ESAPI提供的函數(shù)進(jìn)行JavaScript編碼:
String encodedContent = ESAPI.encoder().encodeForJavaScript
(request.getParameter(“input”));
原則5:在將不可信數(shù)據(jù)插入到Style屬性里時(shí),對(duì)這些數(shù)據(jù)進(jìn)行CSS編碼
當(dāng)需要往Stylesheet,Style標(biāo)簽或者Style屬性里插入不可信數(shù)據(jù)的時(shí)候,需要對(duì)這些數(shù)據(jù)進(jìn)行CSS編碼。傳統(tǒng)印象里CSS不過(guò)是負(fù)責(zé)頁(yè)面樣式的,但是實(shí)際上它比我們想象的要強(qiáng)大許多,而且還可以用來(lái)進(jìn)行各種攻擊。因此,不要對(duì)CSS里存放不可信數(shù)據(jù)掉以輕心,應(yīng)該只允許把不可信數(shù)據(jù)放入到CSS屬性的值部分,并進(jìn)行適當(dāng)?shù)木幋a。除此以外,最好不要把不可信數(shù)據(jù)放到一些復(fù)雜屬性里,比如url, behavior等,只能被IE認(rèn)識(shí)的Expression屬性允許執(zhí)行JavaScript腳本,因此也不推薦把不可信數(shù)據(jù)放到這里。
<style> selector { property : …插入不可信數(shù)據(jù)前,進(jìn)行CSS編碼…} </style> <style> selector { property : ” …插入不可信數(shù)據(jù)前,進(jìn)行CSS編碼… “} </style> <span style=” property : …插入不可信數(shù)據(jù)前,進(jìn)行CSS編碼… ”> … </span>
[編碼規(guī)則]
除了阿拉伯?dāng)?shù)字和字母,對(duì)其他所有的字符進(jìn)行編碼,只要該字符的ASCII碼小于256。編碼后輸出的格式為 \HH (以 \ 開(kāi)頭,HH則是指該字符對(duì)應(yīng)的十六進(jìn)制數(shù)字)
同原則2,原則3,在對(duì)不可信數(shù)據(jù)進(jìn)行編碼的時(shí)候,切忌投機(jī)取巧對(duì)雙引號(hào)等特殊字符進(jìn)行簡(jiǎn)單轉(zhuǎn)義,攻擊者可以想辦法繞開(kāi)這類(lèi)限制。
可以使用ESAPI提供的函數(shù)進(jìn)行CSS編碼:
String encodedContent =
ESAPI.encoder().encodeForCSS(request.getParameter(“input”));
原則6:在將不可信數(shù)據(jù)插入到HTML URL里時(shí),對(duì)這些數(shù)據(jù)進(jìn)行URL編碼
當(dāng)需要往HTML頁(yè)面中的URL里插入不可信數(shù)據(jù)的時(shí)候,需要對(duì)其進(jìn)行URL編碼,如下:
[編碼規(guī)則]
除了阿拉伯?dāng)?shù)字和字母,對(duì)其他所有的字符進(jìn)行編碼,只要該字符的ASCII碼小于256。編碼后輸出的格式為 %HH (以 % 開(kāi)頭,HH則是指該字符對(duì)應(yīng)的十六進(jìn)制數(shù)字)
在對(duì)URL進(jìn)行編碼的時(shí)候,有兩點(diǎn)是需要特別注意的:
1) URL屬性應(yīng)該使用引號(hào)將值部分包圍起來(lái),否則攻擊者可以很容易突破當(dāng)前屬性區(qū)域,插入后續(xù)攻擊代碼
2) 不要對(duì)整個(gè)URL進(jìn)行編碼,因?yàn)椴豢尚艛?shù)據(jù)可能會(huì)被插入到href, src或者其他以URL為基礎(chǔ)的屬性里,這時(shí)需要對(duì)數(shù)據(jù)的起始部分的協(xié)議字段進(jìn)行驗(yàn)證,否則攻擊者可以改變URL的協(xié)議,例如從HTTP協(xié)議改為DATA偽協(xié)議,或者javascript偽協(xié)議。
可以使用ESAPI提供的函數(shù)進(jìn)行URL編碼:
String encodedContent =
ESAPI.encoder().encodeForURL(request.getParameter(“input”));
ESAPI還提供了一些用于檢測(cè)不可信數(shù)據(jù)的函數(shù),在這里我們可以使用其來(lái)檢測(cè)不可信數(shù)據(jù)是否真的是一個(gè)URL:
String userProvidedURL = request.getParameter(“userProvidedURL”); boolean isValidURL = ESAPI.validator().isValidInput (“URLContext”, userProvidedURL, “URL”, 255, false); if (isValidURL) {<a href=”<%= encoder.encodeForHTMLAttribute(userProvidedURL) %>”> </a>}
原則7:使用富文本時(shí),使用XSS規(guī)則引擎進(jìn)行編碼過(guò)濾
Web應(yīng)用一般都會(huì)提供用戶輸入富文本信息的功能,比如BBS發(fā)帖,寫(xiě)博客文章等,用戶提交的富文本信息里往往包含了HTML標(biāo)簽,甚至是JavaScript腳本,如果不對(duì)其進(jìn)行適當(dāng)?shù)木幋a過(guò)濾的話,則會(huì)形成XSS漏洞。但我們又不能因?yàn)楹ε庐a(chǎn)生XSS漏洞,所以就不允許用戶輸入富文本,這樣對(duì)用戶體驗(yàn)傷害很大。
針對(duì)富文本的特殊性,我們可以使用XSS規(guī)則引擎對(duì)用戶輸入進(jìn)行編碼過(guò)濾,只允許用戶輸入安全的HTML標(biāo)簽,如<b>, <i>, <p>等,對(duì)其他數(shù)據(jù)進(jìn)行HTML編碼。需要注意的是,經(jīng)過(guò)規(guī)則引擎編碼過(guò)濾后的內(nèi)容只能放在<div>, <p>等安全的HTML標(biāo)簽里,不要放到HTML標(biāo)簽的屬性值里,更不要放到HTML事件處理屬性里,或者放到<SCRIPT>標(biāo)簽里。
推薦XSS規(guī)則過(guò)濾引擎:OWASP AntiSamp或者Java HTML Sanitizer
總結(jié)
由于很多地方都可能產(chǎn)生XSS漏洞,而且每個(gè)地方產(chǎn)生漏洞的原因又各有不同,所以對(duì)于XSS的防御來(lái)說(shuō),我們需要在正確的地方做正確的事情,即根據(jù)不可信數(shù)據(jù)將要被放置到的地方進(jìn)行相應(yīng)的編碼,比如放到<div>標(biāo)簽之間的時(shí)候,需要進(jìn)行HTML編碼,放到<div>標(biāo)簽屬性里的時(shí)候,需要進(jìn)行HTML屬性編碼,等等。
XSS攻擊是在不斷發(fā)展的,上面介紹的幾條原則幾乎涵蓋了Web應(yīng)用里所有可能出現(xiàn)XSS的地方,但是我們?nèi)匀徊荒艿粢暂p心,為了讓W(xué)eb應(yīng)用更加安全,我們還可以結(jié)合其他防御手段來(lái)加強(qiáng)XSS防御的效果,或者減輕損失:
對(duì)用戶輸入進(jìn)行數(shù)據(jù)合法性驗(yàn)證,例如輸入email的文本框只允許輸入格式正確的email,輸入手機(jī)號(hào)碼的文本框只允許填入數(shù)字且格式需要正確。這類(lèi)合法性驗(yàn)證至少需要在服務(wù)器端進(jìn)行以防止瀏覽器端驗(yàn)證被繞過(guò),而為了提高用戶體驗(yàn)和減輕服務(wù)器壓力,最好也在瀏覽器端進(jìn)行同樣的驗(yàn)證。
為Cookie加上HttpOnly標(biāo)記。許多XSS攻擊的目標(biāo)就是竊取用戶Cookie,這些Cookie里往往包含了用戶身份認(rèn)證信息(比如SessionId),一旦被盜,黑客就可以冒充用戶身份盜取用戶賬號(hào)。竊取Cookie一般都會(huì)依賴(lài)JavaScript讀取Cookie信息,而HttpOnly標(biāo)記則會(huì)告訴瀏覽器,被標(biāo)記上的Cookie是不允許任何腳本讀取或修改的,這樣即使Web應(yīng)用產(chǎn)生了XSS漏洞,Cookie信息也能得到較好的保護(hù),達(dá)到減輕損失的目的。
Web應(yīng)用變得越來(lái)越復(fù)雜,也越來(lái)越容易產(chǎn)生各種漏洞而不僅限于XSS漏洞,沒(méi)有銀彈可以一次性解決所有安全問(wèn)題,我們只能處處留意,針對(duì)不同的安全漏洞進(jìn)行針對(duì)性的防御。
希望本文介紹的幾條原則能幫助你成功防御XSS攻擊,如果你對(duì)于XSS攻擊或防御有任何的見(jiàn)解或疑問(wèn)的話,歡迎留言討論,謝謝。