Web安全從零開始 XSS IV
這是自己寫的 Web 安全從零開始系列之 XSS 篇。第四篇講 XSS 防禦。
[TOC]
Defend
無論是服務端型還是客戶端型xss,攻擊達成都需要兩個條件
- 程式碼被注入
- 程式碼被執行
其實只要做好無論任何情況下保證程式碼不被執行就能完全杜絕 xss 攻擊.
總之, 任何時候都不要把不受信任的資料直接插入到 dom 中的任何位置, 一定要做轉義。
對於某些位置,不受信任的資料做轉義就可以保證安全
- 一般的標籤屬性值(非事件屬性)
- div body 的內部html
對於某些位置,即使做了轉義依然不安全
- script標籤中
- 註釋中
- 表籤的屬性名名
- 標籤名
- css標籤中
使用 JSON.parse 而不是eval, request 的content-type要指定是Content-Type: application/json;
如果連結的URL中部分是動態生成的,一定要做轉義。
Seven Principles
原則1:不要在頁面中插入任何不可信資料,除非這些數已經據根據下面幾個原則進行了編碼
第一條原則其實是“Secure By Default”原則:不要往HTML頁面中插入任何不可信資料,除非這些資料已經根據下面幾條原則進行了編碼。
之所以有這樣一條原則存在,是因為 HTML 裡有太多的地方容易形成XSS漏洞,而且形成漏洞的原因又有差別,比如有些漏洞發生在HTML標籤裡,有些發生在HTML標籤的屬性裡,還有的發生在頁面的 <Script>
裡,甚至有些還出現在CSS裡,再加上不同的瀏覽器對頁面的解析或多或少有些不同,使得有些漏洞只在特定瀏覽器裡才會產生。如果想要通過XSS過濾器(XSS Filter)對不可信資料進行轉義或替換,那麼XSS過濾器的過濾規則將會變得異常複雜,難以維護而且會有被繞過的風險。
所以實在想不出有什麼理由要直接往HTML頁面裡插入不可信資料,就算是有XSS過濾器幫你做過濾,產生XSS漏洞的風險還是很高
<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,但著實減輕了開發人員的負擔。
<body>…插入不可信資料前,對其進行HTML Entity編碼…</body><div>…插入不可信資料前,對其進行HTML Entity編碼…</div><p>…插入不可信資料前,對其進行HTML Entity編碼…</p> 以此類推,往其他HTML標籤之間插入不可信資料前,對其進行HTML Entity編碼
[編碼規則]
那麼HTML Entity編碼具體應該做哪些事情呢?它需要對下面這6個特殊字元進行編碼:
&–>& <–>< >–>> ”–>" ‘–>' /–>/
有兩點需要特別說明的是:
- 不推薦將單引號( ‘ )編碼為 ' 因為它並不是標準的HTML標籤
- 需要對斜槓號( / )編碼,因為在進行XSS攻擊時,斜槓號對於關閉當前HTML標籤非常有用
推薦使用OWASP提供的 ESAPI 函式庫,它提供了一系列非常嚴格的用於進行各種安全編碼的函式。在當前這個例子裡,你可以使用:
String encodedContent = ESAPI.encoder().encodeForHTML(request.getParameter(“input”));
這條原則是指,當你要往HTML屬性(例如width、name、value屬性)的值部分(data value)插入不可信資料的時候,應該對資料進行HTML屬性編碼。不過需要注意的是,當要往HTML標籤的事件處理屬性(例如onmouseover)裡插入資料的時候,本條原則不適用,應該用下面介紹的原則4對其進行JavaScript編碼。
<div attr=…插入不可信資料前,進行HTML屬性編碼…></div>屬性值部分沒有使用引號,不推薦 <div attr=’…插入不可信資料前,進行HTML屬性編碼…’></div> 屬性值部分使用了單引號 <div attr=”…插入不可信資料前,進行HTML屬性編碼…”></div> 屬性值部分使用了雙引號
[編碼規則]
除了空格符可以閉合當前屬性外,這些符號也可以:
% * + , – / ; < = > ^ | `(反單引號,IE會認為它是單引號)
可以使用ESAPI提供的函式進行HTML屬性編碼:
String encodedContent = ESAPI.encoder().encodeForHTMLAttribute(request.getParameter(“input”));
原則4:在將不可信資料插入到SCRIPT裡時,對這些資料進行SCRIPT編碼
這條原則主要針對動態生成的JavaScript程式碼,這包括指令碼部分以及HTML標籤的事件處理屬性(Event Handler,如onmouseover, onload等)。在往JavaScript程式碼裡插入資料的時候,只有一種情況是安全的,那就是對不可信資料進行JavaScript編碼,並且只把這些資料放到使用引號包圍起來的值部分(data value)之中,例如:
<script> var message = “<%= encodeJavaScript(@INPUT) %>”; </script>
除此之外,往JavaScript程式碼裡其他任何地方插入不可信資料都是相當危險的,攻擊者可以很容易地插入攻擊程式碼。
<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攻擊。
可以使用ESAPI提供的函式進行JavaScript編碼:
String encodedContent = ESAPI.encoder().encodeForJavaScript(request.getParameter(“input”));
原則5:在將不可信資料插入到Style屬性裡時,對這些資料進行CSS編碼
當需要往Stylesheet,Style標籤或者Style屬性裡插入不可信資料的時候,需要對這些資料進行CSS編碼。傳統印象裡CSS不過是負責頁面樣式的,但是實際上它比我們想象的要強大許多,而且還可以用來進行各種攻擊。因此,不要對CSS裡存放不可信資料掉以輕心,應該只允許把不可信資料放入到CSS屬性的值部分,並進行適當的編碼。除此以外,最好不要把不可信資料放到一些複雜屬性裡,比如url, behavior等,只能被IE認識的Expression屬性允許執行JavaScript指令碼,因此也不推薦把不可信資料放到這裡。
<style>selector { property : …插入不可信資料前,進行CSS編碼…} </style><style>selector { property : ” …插入不可信資料前,進行CSS編碼… “} </style> <span style=” property : …插入不可信資料前,進行CSS編碼… ”> … </span>
[編碼規則]
除了阿拉伯數字和字母,對其他所有的字元進行編碼,只要該字元的ASCII碼小於256。編碼後輸出的格式為 \HH (以 \ 開頭,HH則是指該字元對應的十六進位制數字)
同原則2,原則3,在對不可信資料進行編碼的時候,切忌投機取巧對雙引號等特殊字元進行簡單轉義,攻擊者可以想辦法繞開這類限制。
可以使用ESAPI提供的函式進行CSS編碼:
String encodedContent = ESAPI.encoder().encodeForCSS(request.getParameter(“input”));
原則6:在將不可信資料插入到HTML URL裡時,對這些資料進行URL編碼
當需要往HTML頁面中的URL裡插入不可信資料的時候,需要對其進行URL編碼,如下:
<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編碼:
String encodedContent = ESAPI.encoder().encodeForURL(request.getParameter(“input”));
ESAPI還提供了一些用於檢測不可信資料的函式,在這裡我們可以使用其來檢測不可信資料是否真的是一個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:使用富文字時,使用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
Summary
- 當輸出點出現在HTML標籤屬性:
< -> < > -> > & -> & " -> " ' -> '
- 當輸出點出現在
<script>
標籤中。這種情況相當危險,不需要考慮xss觸發,只需要考慮編寫js即可
' -> \'; " -> \"; \ -> \\; / -> \/; (換行符) -> \n; (回車符) -> \r;
- 當輸出點出現在body中
< -> < > -> > & -> & " -> " ' -> '
- 當輸出點出現在js事件中(onClick=”你的程式碼”)
< -> < > -> > & -> & " -> " ' -> ' \ -> \\; / -> \/; (換行符) -> \n; (回車符) -> \r;
- 輸出在URL屬性中
<script src="你的程式碼">
- URL編碼
Escape
htmlspecialchars - php
> htmlspecialchars ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = TRUE ]]] ) : string >
將特殊字元轉換為 HTML 實體
string
待轉換的 string 。
flags
位掩碼,由以下某個或多個標記組成,設定轉義處理細節、無效單元序列、文件型別。 預設是 ENT_COMPAT | ENT_HTML401 。
double_encode
關閉 double_encode
時,PHP 不會轉換現有的 HTML 實體, 預設是全部轉換。
encoding
An optional argument defining the encoding used when converting characters.
字元 | 替換後 |
---|---|
& (& 符號) | & |
“ (雙引號) | " ,除非設定了 ENT_NOQUOTES |
‘ (單引號) | 設定了 ENT_QUOTES 後, ' (如果是 ENT_HTML401 ) ,或者 ' (如果是 ENT_XML1 、 ENT_XHTML 或 ENT_HTML5 )。 |
< (小於) | < |
> (大於) | > |
htmlentities - php
本函式各方面都和 htmlspecialchars() 一樣, 除了 htmlentities() 會轉換所有具有 HTML 實體的字元。
Front end
前端過濾
function xssCheck(str,reg){ return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g, function (a, b) { if(b){ return a; }else{ return { '<':'<', '&':'&', '"':'"', '>':'>', "'":' ', }[a] } }) : ''; }
HTTP Header
X-XSS-Protection
可以通過 http 頭控制是否開啟 xss-filter,預設為開啟。
通常情況下, 在http header中加入以下欄位表示啟用 xss-filter。除了 Firefox ,連 IE 8 以上均支援 X-XSS-Protection
X-XSS-Protection: 0 X-XSS-Protection: 1 X-XSS-Protection: 1; mode=block X-XSS-Protection: 1; report=<reporting-uri>
-
0
禁止XSS過濾。
-
1
啟用XSS過濾(通常瀏覽器是預設的)。 如果檢測到跨站指令碼攻擊,瀏覽器將清除頁面(刪除不安全的部分)。
-
1;mode=block
啟用XSS過濾。 如果檢測到攻擊,瀏覽器將不會清除頁面,而是阻止頁面載入。
-
1; report=
(Chromium only) 啟用XSS過濾。 如果檢測到跨站指令碼攻擊,瀏覽器將清除頁面並使用CSP
report-uri
指令的功能傳送違規報告。
如上, 現代瀏覽器都對反射型 xss 有一定的防禦力,其原理是檢查 url 和 dom 中元素的相關性,但這並不能完全防止反射型 xss
另外, 瀏覽器對於儲存型 xss 並沒有抵抗力, 原因很簡單, 使用者的需求是多種多樣的. 所以, 抵禦xss這件事情不能指望瀏覽器。
Content Security Policy
還有就是我們之前介紹的 CSP 策略了。
為了緩解很大一部分潛在的跨站指令碼問題,瀏覽器的擴充套件程式系統引入了 CSP。CSP 管理網站允許載入的內容, 並且使用白名單的機制對網站載入或執行的資源起作用。在網頁中, 這樣的策略通過 HTTP 頭資訊或者 meta 元素定義。
CSP 並不是用來防止 xss 攻擊的,而是最小化 xss 發生後所造成的傷害。實際上, 除了開發者自己做好 xss 轉義, 並沒有別的方法可以防止 xss 的發生. CSP 可以說是 HTML5 給web安全帶來的最實惠的東西。那麼如何引入 CSP 呢?
-
通過響應頭
只允許指令碼從本源載入
Content-Security-Policy: script-src 'self'
-
通過 HTML 的 META 標籤
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
那麼 CSP 除了限制 script-src 之外還能限制什麼呢?
- base-uri : 限制這篇文件的uri
- child-src :限制子視窗的源(iframe,彈窗等),取代 frame-src
- connect-src :限制指令碼可以訪問的源
- font-src : 限制字型的源
- form-action : 限制表單能夠提交到的源
- frame-ancestors : 限制了當前頁面可以被哪些頁面以iframe,frame,object等方式載入
- frame-src :deprecated with child-src,限制了當前頁面可以載入哪些源,與frame-ancestors對應
- img-src : 限制圖片可以從哪些源載入
- media-src : 限制video, audio, source, track 能夠從哪些源載入
- object-src :限制外掛可以從哪些源載入
- sandbox :強制開啟沙盒模式
另外,CSP 還提供一個報告的頭域 Content-Security-Policy-Report-Only
,使用這個頭域,瀏覽器會向伺服器報告 csp 狀態。
Content-Security-Policy-Report-Only: script-src 'self'; report-uri http://cspReport/
使用了上面的設定, 若頁面上存在內聯的 js,它依然會執行,不過瀏覽器會向傳送一個 post 請求,包含如下資訊:
{ "csp-report":{ "document-uri": "http://cspReport/test.php", "referrer": "", "violated-directive": "script-src 'self'", "original-policy": "script-src 'self'; report-uri http://cspReport/", "blocked-uri": "" } }
CSP 目前有兩版, [CSP1][] 和 [CSP2][]. 兩版的支援狀態可以在 http://caniuse.com/#search=csp 中查到. 如下:
CSP 1.0:
CSP 2.0:
X-Frame-Options
X-Frame-Options 有三個值:
-
DENY
表示該頁面不允許在 frame 中展示,即便是在相同域名的頁面中巢狀也不允許。
-
SAMEORIGIN
表示該頁面可以在相同域名頁面的 frame 中展示。
-
ALLOW-FROM *uri*
表示該頁面可以在指定來源的 frame 中展示。
X-Frame-Options 響應頭是用來給瀏覽器指示允許一個頁面可否在 frame
, iframe
或者 object
等標籤中展現的標記. 網站可以使用此功能, 來確保自己網站的內容沒有被嵌到別人的網站中去, 也從而避免了點選劫持 (clickjacking) 的攻擊. 但以後可能被CSP的 frame-ancestors取代。目前支援的狀態比起 CSP frame-ancestors要好。
HttpOnly
當 Cookie 在訊息頭中被設定為 HttpOnly 時,這樣支援 Cookie 的瀏覽器將阻止客戶端 Javascript 直接訪問瀏覽器中的 cookies ,從而達到保護敏感資料的作用。
X-Content-Type-Options
X-Content-Type-Options: nosniff
nosniff
假如請求型別為以下兩種,那麼阻止請求的發生:
- “
style
“ 但是 MIME 型別不是 “text/css
“, - “
script
“ 但是 MIME 型別不是 JavaScript MIME 型別 。
X-Content-Type-Options 響應首部相當於一個提示標誌,被伺服器用來提示客戶端一定要遵循在 Content-Type
首部中對 MIME 型別 的設定,而不能對其進行修改。這就禁用了客戶端的 MIME 型別嗅探 行為,換句話說,也就是意味著網站管理員確定自己的設定沒有問題。