CSRF 的攻擊類型#
CSRF,全稱 Cross Site Request Forgery,跨站請求偽造,也被稱為 XSRF、one-click attack 或者 session riding,是一種劫持受信任用戶向伺服器發送非預期請求的攻擊方式。攻擊者誘導受害者進入第三方網站,在第三方網站中,向被攻擊網站發送跨站請求。利用受害者在被攻擊網站已經獲取的註冊憑證,繞過後台的用戶驗證,達到冒充用戶對被攻擊的網站執行某項操作的目的。與 XSS 相比,XSS 利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。
通常情況下,CSRF 攻擊是攻擊者借助受害者的 Cookie 騙取伺服器的信任,可以在受害者毫不知情的情況下以受害者名義偽造請求發送給受攻擊伺服器,從而在並未授權的情況下執行在權限保護之下的操作。
來自知乎 李向天 的回答中有一段關於 CSRF 的生動描述:
防盜系統啟動:
媽媽:給我看著衣服呀
小孩:好的小偷來了
正常工作:
小孩:你是誰?
小偷:我是張三
小孩:媽媽,有人偷衣服
媽媽:誰?
小孩:張三小偷被抓
漏洞:
小孩:你是誰?
小偷:我叫逗你玩
小孩:媽媽有人偷衣服呀
媽媽:誰?
小孩:逗你玩
媽媽: ...CSRF 是讓用戶在不知情的情況下,冒用其身份發起了一個請求:
小偷:你媽媽喊你去買洗衣粉
CSRF 原理很簡單,甚至較於 XSS 顯得更為單調。具體而言,包括以下 3 種攻擊類型:
- GET 類型的 CSRF
- POST 類型的 CSRF
- 鏈接類型的 CSRF
GET 類型的 CSRF#
這類攻擊非常簡單,只需要一個 HTTP 請求:
<img src="http://a.com/withdraw?amount=10000&for=hacker" >
在受害者訪問含有這個 img 的頁面後,瀏覽器會自動向 a.com 發出一次 HTTP 請求。a.com 就會收到包含受害者登錄信息的一次跨域請求。
POST 類型的 CSRF#
這種類型的 CSRF 利用起來通常使用的是一個自動提交的表單,如:
<form action="http://a.com/withdraw" method=POST>
<input type="hidden" name="account" value="airing" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script>
訪問該頁面後,表單會自動提交,相當於模擬用戶完成了一次 POST 操作。可見這種類型的 CSRF 與第一種一樣,都是模擬請求,所以後端接口也不能將安全寄托在僅允許 POST 請求上。
鏈接類型的 CSRF#
鏈接類型的 CSRF 並不常見,比起其他兩種用戶打開頁面就中招的情況,這種需要用戶點擊鏈接才會觸發,但本質上與前兩種一樣。這種類型通常是在論壇中發布的圖片中嵌入惡意鏈接,或者以廣告的形式誘導用戶中招,攻擊者通常會以比較誇張的詞語誘騙用戶點擊,例如:
<a href="http://a.com/withdraw.php?amount=1000&for=hacker" taget="_blank">
屠龍寶刀,點擊就送!
<a/>
由於之前用戶登錄了信任的網站 A,並且保存登錄狀態,只要用戶主動訪問上面的這個頁面,則表示攻擊成功。
CSRF 的防禦方法#
CSRF 通常從第三方網站發起,被攻擊的網站無法防止攻擊發生,只能通過增強自己網站針對 CSRF 的防護能力來提升安全性。
上文中講了 CSRF 的兩個特點:
- CSRF(通常)發生在第三方域名。
- CSRF 攻擊者不能獲取到 Cookie 等信息,只是使用。
針對以上特點,CSRF 可制定以下兩種防禦策略:
- 自動防禦:阻止不明外域的訪問
- 同源檢測
- Samesite Cookie
- 主動防禦:提交時要求附加本域才能獲取的信息
- Synchrogazer Tokens
- Double Cookie Defense
- Custom Header
自動防禦即利用 HTTP 協議固有的特性進行自動防護,而主動防禦則需要通過編程手段進行防禦。
CSRF 自動防禦策略#
同源檢測#
既然 CSRF 大多來自第三方網站,那麼我們就直接禁止外域 / 不信任的域對我們發起請求。
在 HTTP 協議中,每一個異步請求都會攜帶兩個 Header,用於標記來源域名:
- Origin Header<□ />
- Referer Header<□ />
通過驗證這兩個 Header 是否受信任從而實現同源檢測。但這種方法並非萬無一失,Referer 的值是由瀏覽器提供的,雖然 HTTP 協議上有明確的要求,但是每個瀏覽器對於 Referer 的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方來保障,從理論上來講,這樣並不是很安全。在部分情況下,攻擊者可以隱藏,甚至修改自己請求的 Referer。我們在寫爬蟲之時,也通常會修改 Header 去繞過伺服器的同源檢測。在【基本功】 前端安全系列之二:如何防止 CSRF 攻擊? | 美團技術團隊 一文中具體分析了 Referer 的可信度與危險場景,這裡限於篇幅便不再贅述。
綜上所述,同源驗證是一個相對簡單的防範方法,能夠防範絕大多數的 CSRF 攻擊。但這並不是萬無一失的,對於安全性要求較高,或者有較多用戶輸入內容的網站,我們就要對關鍵的接口做額外的防護措施,也就是下文即將說到的主動防禦策略。
Samesite Cookie#
為了從源頭上解決這個問題,Google 起草了一份草案 來改進 HTTP 協議,那就是為 Set-Cookie 響應頭新增 Samesite 屬性,它用來標明這個 cookie 是個 “同站 cookie”,同站 cookie 只能作為第一方 cookie,不能作為第三方 cookie。SameSite 有兩個屬性值,分別是 Strict 和 Lax。
- Samesite=Strict:嚴格模式,表明這個 cookie 在任何情況下都不可能作為第三方 cookie,絕無例外。
- Samesite=Lax:寬鬆模式,比 Strict 放寬了點限制。假如這個請求是同步請求(改變了當前頁面或者打開了新頁面)且同時是一個 GET 請求,則這個 cookie 可以作為第三方 cookie。
但 Samesite Cookie 也存在著一些問題:
- Samesite 的兼容性不是很好,現階段除了從新版 Chrome 和 Firefox 支持以外,Safari 以及 iOS Safari 都還不支持,現階段看來暫時還不能普及。
- 而且,SamesiteCookie 目前有一個致命的缺陷,不支持子域。例如,種在 blog.ursb.me 下的 Cookie,並不能使用 ursb.me 下種植的 SamesiteCookie。這就導致了當我們網站有多個子域名時,不能使用 SamesiteCookie 在主域名存儲用戶登錄信息。每個子域名都需要用戶重新登錄一次。這是不實際的。
CSRF 主動防禦策略#
CSRF 主動防禦措施有以下三種:
- Synchronizer Tokens:通過響應頁面時將 token 渲染到頁面上,在 form 表單提交的時候通過隱藏域提交上來。
- Double Cookie Defense:將 token 設置在 Cookie 中,在提交 POST 請求的時候提交 Cookie,並通過 header 或者 body 帶上 Cookie 中的 token,服務端進行對比校驗。
- Custom Header:信任帶有特定的 header(例如
X-Requested-With: XMLHttpRequest
)的請求。這個方案可以被繞過,所以 rails 和 django 等框架都放棄了該防範方式。
所以下文主要講講前面兩種防禦方式。
Synchrogazer Token#
Synchrogazer Token,即同步表單的 CSRF 校驗。CSRF 攻擊之所以能夠成功,是因為伺服器誤把攻擊者發送的請求當成了用戶自己的請求。那么我們可以要求所有的用戶請求都攜帶一個 CSRF 攻擊者無法獲取到的 Token。伺服器通過校驗請求是否攜帶正確的 Token,來把正常的請求和攻擊的請求區分開,也可以防範 CSRF 的攻擊。
具體而言,分為以下三個步驟:
- 將 CSRF Token 輸出到頁面中
- 頁面提交的請求攜帶這個 Token,通常隱藏在表單域中作為參數提交,或拼接在 URL 後作為 query 提交。
- 伺服器驗證 Token 是否正確
當用戶從客戶端得到了 Token,再次提交給伺服器的時候,伺服器需要判斷 Token 的有效性,驗證過程是先解密 Token,對比加密字符串以及時間戳,如果加密字符串一致且時間未過期,那麼這個 Token 就是有效的。這種 Token 的值通常是使用 UserID、時間戳和隨機數,通過加密的方法生成。這樣的加密既能驗證請求的用戶、請求的時間,又能保證 Token 不容易被破解。個人在項目中使用以下加密方式,仅供参考:
import md5 from 'md5'
export const MESSAGE = {
OK: {
code: 0,
message: '請求成功',
},
TOKEN_ERROR: {
code: 403,
message: 'TOKEN失效',
},
}
const md5Pwd = (password) => {
const salt = 'Airing_is_genius'
return md5(md5(password + salt))
}
export const validate = (res, check, ...params) => {
for (let param of params) {
if (typeof param === 'undefined' || param === null) {
return res.json(MESSAGE.PARAMETER_ERROR)
}
}
if (check) {
const uid = params[0]
const timestamp = params[1]
const token = params[2]
if (token !== md5Pwd(uid.toString() + timestamp.toString() + KEY))
return res.json(MESSAGE.TOKEN_ERROR)
}
}
這種方法要比之前檢查 Referer 或者 Origin 要安全一些,Token 可以在產生並放於 Session 之中,然後在每次請求時把 Token 從 Session 中拿出,與請求中的 Token 進行比對。但是有以下兩點需要注意。
- Session Vs Cookie若可以將 token 存放到 Session 中,卻是一個不錯的選擇 **。
- 刷新 CSRF Token:當 CSRF token 存儲在 Cookie 中時,一旦在同一個瀏覽器上發生用戶切換,新登錄的用戶將會依舊使用舊的 token(之前用戶使用的),這會帶來一定的安全風險,因此在每次用戶登錄的時候都必須刷新 CSRF token。
Double Cookie Defence#
Double Cookie Defence,中文譯作雙重 Cookie 驗證。
在 Session 中存儲 CSRF Token 比較繁瑣,而且不能在通用的攔截上統一處理所有的接口。那么另一種防禦措施是使用雙重提交 Cookie。利用 CSRF 攻擊不能獲取到用戶 Cookie 的特點,我們可以要求 Ajax 和表單請求攜帶一個 Cookie 中的值。
- 在用戶訪問網站頁面時,向請求域名注入一個 Cookie,內容為隨機字符串。
- 在前端向後端發起請求時,取出 Cookie,並添加到 URL 的參數中。
- 後端接口驗證 Cookie 中的字段與 URL 參數中的字段是否一致,不一致則拒絕。
此方法相對於 CSRF Token 就簡單了許多。可以直接通過前後端攔截的方法自動化實現。後端校驗也更加方便,只需進行請求中字段的對比,而不需要再進行查詢和存儲 Token。但是它並沒有被大規模應用,尤其在大型網站上,存在著嚴重的缺陷。舉一個栗子:
由於任何跨域都會導致前端無法獲取 Cookie 中的字段(包括子域名之間),所以當用戶訪問我的 me.ursb.me 之時,由於我的後端 api 部署在 api.ursb.me 上,那麼在 me.ursb.me 用戶拿不到 api.ursb.me 的 Cookie,也就無法完成雙重 Cookie 驗證。依此,我們的 Cookie 放在了 ursb.me 主域名下,以保證每個子域名都可以訪問。但 ursb.me 下其實我還部署了很多其他的子應用,如果某個子域名 xxx.ursb.me 存在漏洞,雖然這個 xxx.ursb.me 可能沒有什麼值得竊取的信息,但是攻擊者可以修改 ursb.me 下的 Cookie,從而實現 XSS 攻擊,並利用篡改的 Cookie 對 me.ursb.me 發起 CSRF 攻擊。同時,為了確保 Cookie 傳輸安全,採用這種防禦方式的最好確保用整站 HTTPS 的方式,如果還沒切 HTTPS 的使用這種方式會有風險。
以下是來自 【基本功】 前端安全系列之二:如何防止 CSRF 攻擊? | 美團技術團隊 的關於雙重 Token 驗證的總結:
優點:
- 無需使用 Session,適用面更廣,易於實施。
- Token 儲存於客戶端中,不會給伺服器帶來壓力。
- 相對於 Token,實施成本更低,可以在前後端統一攔截校驗,而不需要一個個接口和頁面添加。
缺點:
- Cookie 中增加了額外的字段。
- 如果有其他漏洞(例如 XSS),攻擊者可以注入 Cookie,那麼該防禦方式失效。
- 難以做到子域名的隔離。
- 為了確保 Cookie 傳輸安全,採用這種防禦方式的最好確保用整站 HTTPS 的方式,如果還沒切 HTTPS 的使用這種方式也會有風險。