1. 程式人生 > >XSS 和 CSRF簡述及預防措施

XSS 和 CSRF簡述及預防措施

在 Web 安全領域中,XSS 和 CSRF 是最常見的攻擊方式。本文將會簡單介紹 XSS 和 CSRF 的攻防問題。

1. xss

XSS,即 Cross Site Script,中譯是跨站指令碼攻擊;其原本縮寫是 CSS,但為了和層疊樣式表(Cascading Style Sheet)有所區分,因而在安全領域叫做 XSS。

XSS 攻擊是指攻擊者在網站上注入惡意的客戶端程式碼,通過惡意指令碼對客戶端網頁進行篡改,從而在使用者瀏覽網頁時,對使用者瀏覽器進行控制或者獲取使用者隱私資料的一種攻擊方式。

攻擊者對客戶端網頁注入的惡意指令碼一般包括 JavaScript,有時也會包含 HTML 和 Flash。有很多種方式進行 XSS 攻擊,但它們的共同點為:將一些隱私資料像 cookie、session 傳送給攻擊者,將受害者重定向到一個由攻擊者控制的網站,在受害者的機器上進行一些惡意操作。

XSS攻擊可以分為3類:反射型(非持久型)、儲存型(持久型)、基於DOM。

1.1反射性

反射型 XSS 只是簡單地把使用者輸入的資料 “反射” 給瀏覽器,這種攻擊方式往往需要攻擊者誘使使用者點選一個惡意連結,或者提交一個表單,或者進入一個惡意網站時,注入指令碼進入被攻擊者的網站。

看一個示例。我先準備一個如下的靜態頁:

惡意連結的地址指向了 localhost:8001/?q=111&p=222。然後,我再啟一個簡單的 Node 服務處理惡意連結的請求:

const http = require('http');
function handleReequest(req, res) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
    res.write('<script>alert("反射型 XSS 攻擊")</script>');
    res.end();
}

const server = new http.Server();
server.listen(8001, '127.0.0.1');
server.on('request', handleReequest);

當用戶點選惡意連結時,頁面跳轉到攻擊者預先準備的頁面,會發現在攻擊者的頁面執行了 js 指令碼:

這樣就產生了反射型 XSS 攻擊。攻擊者可以注入任意的惡意指令碼進行攻擊,可能注入惡作劇指令碼,或者注入能獲取使用者隱私資料(如cookie)的指令碼,這取決於攻擊者的目的。

1.2儲存型

儲存型 XSS 會把使用者輸入的資料 “儲存” 在伺服器端,當瀏覽器請求資料時,指令碼從伺服器上傳回並執行。這種 XSS 攻擊具有很強的穩定性。

比較常見的一個場景是攻擊者在社群或論壇上寫下一篇包含惡意 JavaScript 程式碼的文章或評論,文章或評論發表後,所有訪問該文章或評論的使用者,都會在他們的瀏覽器中執行這段惡意的 JavaScript 程式碼。

舉一個示例。

先準備一個輸入頁面:

<input type="text" id="input">
<button id="btn">Submit</button>  
<script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');

    let val;
    input.addEventListener('change', (e) => {
        val = e.target.value;
    },false);
    btn.addEventListener('click', (e) => {
        fetch('http://localhost:8001/save', {
            method: 'POST',
            body: val
        });
    }, false);
</script>

啟動一個 Node 服務監聽 save 請求。為了簡化,用一個變數來儲存使用者的輸入:

const http = require('http');
let userInput = '';

function handleReequest(req, res) {
    const method = req.method;
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
    if (method === 'POST' && req.url === '/save'){
        let body = '';
        req.on('data',chunk => {
            body += chunk;
        });
        req.on('end', () => {
            if (body) {
                userInput = body;
            }
            res.end();
        });
    } else {
        res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
        res.write(userInput);  //將輸入的指令碼內容返回給前端頁面
        res.end();
    }
}

const server = new http.Server();
server.listen(8001, '127.0.0.1');
server.on('request', handleReequest);

當用戶點選提交按鈕將輸入資訊提交到服務端時,服務端通過 userInput 變數儲存了輸入內容。當用戶通過 http://localhost:8001/${id} 訪問時,服務端會返回與 id 對應的內容(本示例簡化了處理)。如果使用者輸入了惡意指令碼內容,則其他使用者訪問該內容時,惡意指令碼就會在瀏覽器端執行:

1.3 基於DOM

基於 DOM 的 XSS 攻擊是指通過惡意指令碼修改頁面的 DOM 結構,是純粹發生在客戶端的攻擊。

看如下程式碼:

<h2>XSS: </h2>
<input type="text" id="input">
<button id="btn">Submit</button>
<div id="div"></div>
<script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');
    const div = document.getElementById('div');
    let val;
    input.addEventListener('change', (e) => {
        val = e.target.value;
    }, false);
    btn.addEventListener('click', () => {
        div.innerHTML = `<a href=${val}>testLink</a>`
    }, false);
</script>

點選 Submit 按鈕後,會在當前頁面插入一個連結,其地址為使用者的輸入內容。如果使用者在輸入時構造瞭如下內容:

'' onclick=alert(/xss/)

使用者提交之後,頁面程式碼就變成了:

<a href onlick="alert(/xss/)">testLink</a>

此時,使用者點選生成的連結,就會執行對應的指令碼:

2.XSS攻擊的防範

現在主流的瀏覽器內建了防範 XSS 的措施,例如 CSP。但對於開發者來說,也應該尋找可靠的解決方案來防止 XSS 攻擊。

2.1 HttpOnly 防止劫取 Cookie

HttpOnly 最早由微軟提出,至今已經成為一個標準。瀏覽器將禁止頁面的Javascript 訪問帶有 HttpOnly 屬性的Cookie。

上文有說到,攻擊者可以通過注入惡意指令碼獲取使用者的 Cookie 資訊。通常 Cookie 中都包含了使用者的登入憑證資訊,攻擊者在獲取到 Cookie 之後,則可以發起 Cookie 劫持攻擊。所以,嚴格來說,HttpOnly 並非阻止 XSS 攻擊,而是能阻止 XSS 攻擊後的 Cookie 劫持攻擊。

2.2 輸入檢查

不要相信使用者的任何輸入。 對於使用者的任何輸入要進行檢查、過濾和轉義。建立可信任的字元和 HTML 標籤白名單,對於不在白名單之列的字元或者標籤進行過濾或編碼。

在 XSS 防禦中,輸入檢查一般是檢查使用者輸入的資料中是否包含 <,> 等特殊字元,如果存在,則對特殊字元進行過濾或編碼,這種方式也稱為 XSS Filter。

而在一些前端框架中,都會有一份 decodingMap, 用於對使用者輸入所包含的特殊字元或標籤進行編碼或過濾,如 <,>,script,防止 XSS 攻擊:

// vuejs 中的 decodingMap
// 在 vuejs 中,如果輸入帶 script 標籤的內容,會直接過濾掉
const decodingMap = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"',
  '&amp;': '&',
  '
  ': '\n'
}

2.3輸出檢查

使用者的輸入會存在問題,服務端的輸出也會存在問題。一般來說,除富文字的輸出外,在變數輸出到 HTML 頁面時,可以使用編碼或轉義的方式來防禦 XSS 攻擊。例如利用 sanitize-html 對輸出內容進行有規則的過濾之後再輸出到頁面中。

3.CSRF

CSRF,即 Cross Site Request Forgery,中譯是跨站請求偽造,是一種劫持受信任使用者向伺服器傳送非預期請求的攻擊方式。

通常情況下,CSRF 攻擊是攻擊者藉助受害者的 Cookie 騙取伺服器的信任,可以在受害者毫不知情的情況下以受害者名義偽造請求傳送給受攻擊伺服器,從而在並未授權的情況下執行在許可權保護之下的操作。

在舉例子之前,先說說瀏覽器的 Cookie 策略

3.1 瀏覽器的 Cookie 策略

Cookie 是伺服器傳送到使用者瀏覽器並儲存在本地的一小塊資料,它會在瀏覽器下次向同一伺服器再發起請求時被攜帶併發送到伺服器上。Cookie 主要用於以下三個方面:

  • 會話狀態管理(如使用者登入狀態、購物車、遊戲分數或其它需要記錄的資訊)

  • 個性化設定(如使用者自定義設定、主題等)

  • 個性化設定(如使用者自定義設定、主題等)

而瀏覽器所持有的 Cookie 分為兩種:

  • Session Cookie(會話期 Cookie):會話期 Cookie 是最簡單的Cookie,它不需要指定過期時間(Expires)或者有效期(Max-Age),它僅在會話期內有效,瀏覽器關閉之後它會被自動刪除。

  • Permanent Cookie(永續性 Cookie):與會話期 Cookie 不同的是,永續性 Cookie 可以指定一個特定的過期時間(Expires)或有效期(Max-Age)。

res.setHeader('Set-Cookie', ['mycookie=222', 'test=3333; expires=Sat, 21 Jul 2018 00:00:00 GMT;']);

上述程式碼建立了兩個 Cookie:mycookie 和 test,前者屬於會話期 Cookie,後者則屬於永續性 Cookie。當我們去檢視 Cookie 相關的屬性時,不同的瀏覽器對會話期 Cookie 的 Expires 屬性值會不一樣:

此外,每個 Cookie 都會有與之關聯的域,這個域的範圍一般通過 donmain 屬性指定。如果 Cookie 的域和頁面的域相同,那麼我們稱這個 Cookie 為第一方 Cookie(first-party cookie),如果 Cookie 的域和頁面的域不同,則稱之為第三方 Cookie(third-party cookie)。一個頁面包含圖片或存放在其他域上的資源(如圖片)時,第一方的 Cookie 也只會傳送給設定它們的伺服器。

3.2 通過 Cookie 進行 CSRF 攻擊

假設有一個 bbs 站點:http://www.c.com,當登入後的使用者發起如下 GET 請求時,會刪除 ID 指定的帖子:

http://www.c.com:8002/content/delete/:id

如發起 http://www.c.com:8002/content/delete/87343 請求時,會刪除 id 為 87343 的帖子。當用戶登入之後,會設定如下 cookie:

res.setHeader('Set-Cookie', ['user=22333; expires=Sat, 21 Jul 2018 00:00:00 GMT;']);

user 對應的值是使用者 ID。然後構造一個頁面 A:

CSRF 攻擊者準備的網站:

<p>CSRF 攻擊者準備的網站:</p>
<img src="http://www.c.com:8002/content/delete/87343">

頁面 A 使用了一個 img 標籤,其地址指向了刪除使用者帖子的連結:

可以看到,當登入使用者訪問攻擊者的網站時,會向 www.c.com 發起一個刪除使用者帖子的請求。此時若使用者在切換到 www.c.com 的帖子頁面重新整理,會發現ID 為 87343 的帖子已經被刪除。

由於 Cookie 中包含了使用者的認證資訊,當用戶訪問攻擊者準備的攻擊環境時,攻擊者就可以對伺服器發起 CSRF 攻擊。在這個攻擊過程中,攻擊者藉助受害者的 Cookie 騙取伺服器的信任,但並不能拿到 Cookie,也看不到 Cookie 的內容。而對於伺服器返回的結果,由於瀏覽器同源策略的限制,攻擊者也無法進行解析。因此,攻擊者無法從返回的結果中得到任何東西,他所能做的就是給伺服器傳送請求,以執行請求中所描述的命令,在伺服器端直接改變資料的值,而非竊取伺服器中的資料。

但若 CSRF 攻擊的目標並不需要使用 Cookie,則也不必顧慮瀏覽器的 Cookie 策略了。

4.CSRF 攻擊的防範

當前,對 CSRF 攻擊的防範措施主要有如下幾種方式。

4.1 驗證碼

驗證碼被認為是對抗 CSRF 攻擊最簡潔而有效的防禦方法。

從上述示例中可以看出,CSRF 攻擊往往是在使用者不知情的情況下構造了網路請求。而驗證碼會強制使用者必須與應用進行互動,才能完成最終請求。因為通常情況下,驗證碼能夠很好地遏制 CSRF 攻擊。

但驗證碼並不是萬能的,因為出於使用者考慮,不能給網站所有的操作都加上驗證碼。因此,驗證碼只能作為防禦 CSRF 的一種輔助手段,而不能作為最主要的解決方案。

4.2 Referer Check

根據 HTTP 協議,在 HTTP 頭中有一個欄位叫 Referer,它記錄了該 HTTP 請求的來源地址。通過 Referer Check,可以檢查請求是否來自合法的”源”。

比如,如果使用者要刪除自己的帖子,那麼先要登入 www.c.com,然後找到對應的頁面,發起刪除帖子的請求。此時,Referer 的值是 http://www.c.com;當請求是從 www.a.com 發起時,Referer 的值是 http://www.a.com 了。因此,要防禦 CSRF 攻擊,只需要對於每一個刪帖請求驗證其 Referer 值,如果是以 www.c.com 開頭的域名,則說明該請求是來自網站自己的請求,是合法的。如果 Referer 是其他網站的話,則有可能是 CSRF 攻擊,可以拒絕該請求。

針對上文的例子,可以在服務端增加如下程式碼:

if (req.headers.referer !== 'http://www.c.com:8002/') {
    res.write('csrf 攻擊');
    return;
}

Referer Check 不僅能防範 CSRF 攻擊,另一個應用場景是 “防止圖片盜鏈”。

4.3 新增 token 驗證(token==令牌)

CSRF 攻擊之所以能夠成功,是因為攻擊者可以完全偽造使用者的請求,該請求中所有的使用者驗證資訊都是存在於 Cookie 中,因此攻擊者可以在不知道這些驗證資訊的情況下直接利用使用者自己的 Cookie 來通過安全驗證。要抵禦 CSRF,關鍵在於在請求中放入攻擊者所不能偽造的資訊,並且該資訊不存在於 Cookie 之中。可以在 HTTP 請求中以引數的形式加入一個隨機產生的 token,並在伺服器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內容不正確,則認為可能是 CSRF 攻擊而拒絕該請求。

總結

本文主要介紹了 XSS 和 CSRF 的攻擊原理和防禦措施。當然,在 Web 安全領域,除了這兩種常見的攻擊方式,也存在這 SQL 注入等其它攻擊方式,這不在本文的討論範圍之內,如果你對其感興趣,可以閱讀SQL注入技術專題的專欄詳細瞭解相關資訊。最後,總結一下 XSS 攻擊和 CSRF 攻擊的常見防禦措施:

1.防禦XSS攻擊

  • HttpOnly 防止劫取 Cookie

  • 使用者的輸入檢查

  • 服務端的輸出檢查

2.防禦CSRF攻擊

  •   驗證碼

  • Referer Check

  • Token 驗證

參考資料

  • Cross-site scripting

  • CSRF 攻擊的應對之道

  • 《白帽子講 Web 安全》