跨站腳本(XSS, Cross Site Script)攻擊指的是,攻擊者可讓某網站執行一段非法腳本。這種狀況很常見,好比提交一個表單用於修改用戶名,咱們能夠在文本框中輸入一些特殊字符,好比 <, >, ', "
等,檢查一下用戶名是否正確修改了。javascript
XSS 必定是由用戶的輸入引發的,不管是提交表單、仍是點擊連接(參數)的方式,只要是對用戶的輸入不作任何轉義就寫到數據庫,或者寫到 html
,js
中,就頗有可能出錯。舉兩個例子。css
假設須要顯示一個新聞標題的列表,服務端渲染的話,用 jade
來實現的話也許是這樣的html
h1 娛樂速遞 ul each val in newslist li= val
newslist
就當是 ['新聞1', '新聞2', ...] 這樣格式的數組,若是直接把內容迭代渲染到 html
上的話,一旦某個新聞標題有特殊字符,好比標題中剛好包含一個 <p>
標籤,那麼它就不會顯示出來。前端
另外一個例子,用戶在寫博客,先不考慮實時保存吧,如今就僅僅須要預覽一下,那麼可能的代碼就是java
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 自己是一個文本文檔,但在瀏覽器中卻能夠顯現得花樣百出,是由於不少字符對於瀏覽器來講是有特殊含義的,好比在 <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 中直接傳輸 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 參數進行轉義的緣由,由於服務端表現良好。
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 進行轉義。