跨站指令碼(XSS, Cross Site Script)攻擊指的是,攻擊者可以讓某網站執行一段非法指令碼。這種情況很常見,比如提交一個表單用於修改使用者名稱,我們可以在文字框中輸入一些特殊字元,比如 <, >, ', "
等,檢查一下使用者名稱是否正確修改了。
XSS 如何發生
XSS 一定是由使用者的輸入引起的,無論是提交表單、還是點選連結(引數)的方式,只要是對使用者的輸入不做任何轉義就寫到資料庫,或者寫到 html
,js
中,就很有可能出錯。舉兩個例子。
假設需要顯示一個新聞標題的列表,服務端渲染的話,用 jade
來實現的話也許是這樣的
h1 娛樂速遞
ul
each val in newslist
li= val
newslist
就當是 ['新聞1', '新聞2', ...] 這樣格式的陣列,如果直接把內容迭代渲染到 html
上的話,一旦某個新聞標題有特殊字元,比如標題中恰好包含一個 <p>
標籤,那麼它就不會顯示出來。
另一個例子,使用者在寫部落格,先不考慮實時儲存吧,現在就僅僅需要預覽一下,那麼可能的程式碼就是
var preview = document.getElementById('#preview'),
title = document.getElementById('#blog-title'),
content = document.getElementById('#blog-content');
preview.innerHTML =
'<h1>' + title.value + '</h1>' +
'<pre>' + content.value + '</pre>';
這裡同樣是把使用者的輸入直接顯示在了 html
上,如果使用者的輸入中,正好輸入了 </h1>
,把 <h1>
標籤提前結束,然後再輸入 <script>...</script>
就可以直接執行 js
程式碼了。
XSS 的發生至少需要一個條件,就是這些非法的指令碼必須得在瀏覽器中解析。
從一個請求發出開始,到瀏覽器顯示內容,與 XSS 相關的有三個地方:URL、HTML、JavaScript。至於後臺方面,它分兩個功能,一個是將資料寫到資料庫,這時候也要對資料進行轉義,但不是XSS的範疇,它更多是防止資料破壞 SQL 語句的結構;另一個是從資料庫讀取資料,直接生成 HTML 或者以 JSON 的方式傳給前端,這些資料都必須轉義後才能顯示到瀏覽器中。
HTML 特殊字元
HTML 本身是一個文字文件,但在瀏覽器中卻可以顯現得花樣百出,是因為很多字元對於瀏覽器來說是有特殊含義的,比如在 <script>
中的內容,瀏覽器會做一些動畫等等。那麼對這些特殊字元進行轉義,就意味著讓瀏覽器對待它們的時候,就像普通字元一樣,比如 ≶script>
這段文字在瀏覽器中就會正常顯示為 <script>
。
當我們在程式碼中生成 HTML 時,一定要注意,變數是否轉義了。像這種
el.innerHTML = title.value;
就是非常危險的。因為輸入框的內容來源於使用者,而使用者的輸入是不可靠的。無論是前端還是後臺,一定要有一個類似於 escapeHTML 的方法,然後在程式碼中這樣使用
el.innerHTML = escapeHTML(title.value);
這邊貼一段簡單的用來轉義 HTML 的 JavaScript 方法
function encodeHTML (a) {
return String(a)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
那麼有哪些字元需要轉義呢?這裡列了一些常見的。
" --> "
# --> #
$ --> $
& --> &
' --> '
( --> (
) --> )
; --> ;
< --> <
> --> >
在 escapeHTML 方法中,我使用了別名的方式轉義,因為它比較容易記一點。無論是別名還是十六進位制,它們表示的含義都是一樣的,比如 &
和 &
都表示 &
符號。想要看更具體的列表可以參考這個網站。
在瀏覽器收到 HTML 之後,首先會對所有的內容進行解碼,它會把所有能識別的編碼符號,解碼成字面值。比如有
<p>my name is: <a href="http://www.jchen.cc">名一</a></p>
經過瀏覽器解碼就變成
<p>my name is: <a href="http://www.jchen.cc">名一</a></p>
這裡要說的是,瀏覽器只會對兩個地方解碼,一個是標籤的內容(即 textContent,除了 <script>
和 <style>
標籤),另一個是標籤的屬性值。對於屬性名是不會解碼的。
URL
早些時候,服務端還不支援在 URL 中直接傳輸 Unicode,比如 http://jchen.cc/find?q=你好
這樣的地址,服務端無法識別“你好”這個值,所以必須編碼之後進行傳輸。
那麼對於 URL,我們只需要對引數的值進行編碼就可以了。比如上面這個連結,編碼之後就是 http://jchen.cc/find?q=%E4%BD%A0%E5%A5%BD
。
如果對整個 URL 編碼,那麼連結就無效了。
編碼的方式很簡單,瀏覽器提供了全域性的 encodeURI
方法,呼叫之後就可以實現轉義了。
有一點很重要,encodeURI
是不會轉義 :
, /
, ?
, &
, =
這些在 URL 中有特殊含義的字元的,那麼如果有個引數正好包含了這些字元,就不會轉義,比如
encodeURI('http://jchen.cc/login?name=名一&from=http://other.com');
// -> http://jchen.cc/login?name=%E5%90%8D%E4%B8%80&from=http://other.com
from 引數的值並沒有轉義,這時候,就需要用到另一個方法 encodeURIComponent
var param = encodeURIComponent('http://other.com');
encodeURI('http://jchen.cc/login?name=名一&from=') + param;
// -> http://jchen.cc/login?name=%E5%90%8D%E4%B8%80&from=http%3A%2F%2Fother.com
所以結論就是,如果要對整個 URL 進行轉義,使用 encodeURI
,如果對引數的值進行轉義,使用 encodeURIComponent
。
當動態生成的連結地址需要賦值給 href 或者 src 屬性時,需要對這些地址進行 URL 轉義。當然,如果服務端支援在 URL 中包含 UTF-8 的字元的話,其實不轉義也不會錯,這就是為什麼我們平時不會太注意對錶單和 URL 引數進行轉義的原因,因為服務端表現良好。
JavaScript 特殊字元
JS 中的轉義都是通過反斜槓完成,有三種型別,以 '
和 "
為例
直接反斜槓 --> \'\"
十六進位制 --> \x22\x27
Unicode --> \u0022\u0027
一般情況下可以直接通過反斜槓轉義,但有些字元我們不知道怎麼輸入,很常見的比如 Web Font,在 CSS 中可以看到類似這樣的程式碼
.glyphicon-home::before {
content: "";
}
那個 content 中的值可以通過十六進位制或者 Unicode 的方式來代替。
JS 轉義一般用於顯示使用者輸入的時候,比如使用者輸入了反斜槓,需要顯示時,就必須 alert('\\');
。
解碼順序
當瀏覽器進行繪製時,首先會對 HTML 進行解碼,然後是 URL,最後是執行 JS 時對它進行解碼。
現在考慮這三種編碼同時存在的情況
<a href="javascript: alert('\<http://jchen.cc/find?q=%E4%BD%A0%E5%A5%BD\>');">click</a>
首先是 HTML 解碼,結果為
<a href="javascript: alert('\<http://jchen.cc/find?q=%E4%BD%A0%E5%A5%BD\>');">click</a>
然後是 URL 解碼,結果為
<a href="javascript: alert('\<http://jchen.cc/find?q=你好\>');">click</a>
最後是 JS 解碼,結果為
<a href="javascript: alert('<http://jchen.cc/find?q=你好>');">click</a>
單擊連結後,應該會出現一個彈窗,內容是 <http://jchen.cc/find?q=你好>
。
本文更多的是介紹如何防止XSS的發生,而不是它的危害。核心就是用適當的方法對 HTML, JS 進行轉義。