Skip to content

[Security] 認識 CSRF 與常見防禦方式

· 15 min

前言#

承接上篇 [Security] CORS 與跨來源資源安全性問題,此篇主要敘述的是《Beyond XSS:探索網頁前端資安宇宙》 4–4 章節的筆記,若有錯誤歡迎大家回覆告訴我~

CSRF 基本介紹#

前面提到的 CORS 重點在「讀取」,若 CORS 設置錯誤,攻擊者可讀取跨來源資料,這個資料可能是使用者資料或其他機密資料;而 CSRF(Cross-Site Request Forgery)跨來源請求偽造的重點在「執行操作」。

在介紹 CSRF 之前,不知道會不會太晚的補充一下什麼是 cookie,其實是因為讀了 Huli 這篇 白話 Session 與 Cookie:從經營雜貨店開始 文章後驚為天人,推薦對 session 和 cookie 不熟的都去讀! 簡單來說有些網站會把某些資料存在使用者的瀏覽器裡面,這些資料就稱為 cookie,當瀏覽器對這些網站發 request 時就會把之前儲存的 cookie 一併帶上去,收到 request 的網站就可以拿到這些 cookie,可以拿這 cookie 來辨識發出 request 的人是誰、或是他的喜好等 (ref: 利用 Cookie 特性進行的 DoS 攻擊:Cookie 炸彈),示意圖如下:

cookie 運作示意圖

接著以刪除文章功能來介紹 CSRF,刪除文章功能的實作方式很多,例如:點「刪除」後發 API、點「刪除」後送出表單或是把功能做成 GET 用連結完成,而在這範例中採用簡單版,以 GET 連結完成刪除功能,當使用者點下連結就會發刪除的請求:

<a href='/delete?id=3'>刪除</a>

後端收到請求後會再判斷請求是否帶 session id,以及此文章是不是這 id 的作者寫的,以確認「只有作者本人可刪除自己的文章」。

「刪除功能做成 GET ,用連結就能完成」有何問題?
有可能會在作者本人不知情狀況下刪除,假設小黑做一個心理測驗網站並傳給小明,心理測驗網站的「開始測驗」按鈕長這樣:

<a href='https://small-min.blog.com/delete?id=3'>開始測驗</a>

當小明點「開始測驗」按鈕後,瀏覽器就會發 GET 請求給 https://small-min.blog.com/delete?id=3,而根據前面所說的瀏覽器 cookie 運行機制,會一併把 small-min.blog.com 的 cookie 帶上,伺服器透過 cookie 驗證是小明發出請求,且文章作者也是小明,因此將文章刪除。而這就是 CSRF 攻擊,中文稱作跨站請求偽造。

因為點 a 連結後就會讓小明察覺,有個不被察覺的進階版是用看不到的圖片發出請求,避免小明察覺,程式碼如下。

<img src='https://small-min.blog.com/delete?id=3' width='0' height='0' />
<a href='/test'>開始測驗</a>

上述流程的示意圖如下:

CSRF 攻擊流程,以刪除文章功能為例

CSRF(跨站請求偽造)就像是這個案例,小明在心理測驗網站(https://test.com),卻在不知情狀況下刪除 https://small-min.blog.com 的文章,也因此 CSRF 又稱 one-click attack。CSRF 目的是「在其他網站下對目標網站送出請求,讓目標網站誤以為這請求是使用者自己發出的,但其實不是」。

而 CSRF 成立前提是因為在瀏覽器機制中,只要發 request 給某網站,就會把關聯的 cookie 帶上,若使用者是登入狀態,request 會包含他的資訊(e.g. session id),從 request 提供的資訊看起來會像是本人發出的,因為伺服器只依據 cookie 資訊判斷。舉例來說,從 A 網站對 B 發 request,會帶 B 的 cookie;從 C 網站對 B 發 request,會帶 B 的 cookie,示意圖如下:

CSRF 攻擊可能導致使用者的損失,如果這操作行為是銀行轉帳,攻擊者只要在自己網頁寫轉帳給自己帳號的 code 再散布出去,就可收到很多受害者的錢。

POST 仍有 CSRF 問題#

避免上述的 CSRF 攻擊方式,書中有提到可以改用 POST 來實作刪除功能,但仍會有問題,因 <form> 可用來發 POST request,點「開始測驗」後仍會中招:

<form action="https://small-min.blog.com/delete" method="POST">
<input type="hidden" name="id" value="3"/>
<input type="submit" value="開始測驗"/>
</form>

另也可參考 Example of silently submitting a POST FORM (CSRF) 這裡有提到不被察覺的進階版,可建立看不見的 iframe,form submit 後的結果出現在 iframe 內,且 form 可自動 submit,小明不會察覺:

<iframe style="display:none" name="csrf-frame"></iframe>
<form method='POST' action='https://small-min.blog.com/delete' target="csrf-frame" id="csrf-form">
<input type='hidden' name='id' value='3'>
<input type='submit' value='submit'>
</form>
<script>document.getElementById("csrf-form").submit()</script>

由上可知 POST 仍有 CSRF 問題。

將 API 改用 JSON 格式仍有 CSRF 問題#

將 API 改為 JSON 格式,攻擊者就無法用 form 發出 request。不過書中有提到,JSON 格式的 body 也可用表單拼出。對某些伺服器,若 request 的 content type 非 application/json,而 body 是 JSON 格式,伺服器會拋出錯誤,不認為是合法 request;但對另外一些伺服器,若 request 的 content type 是 text/plain ,但 body 是 JSON 格式,則是可以接受的,因此可以用以下方式送出請求:

<form action="https://small-min.blog.com/delete" method="post" enctype="text/plain">
<input name='{"id":3, "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="delete!"/>
</form>

<form> 產生 request body 的規則是 name=value,上面這段表單的 request body 是:

{"id":3, "ignore_me":"=test"}

因此將 API 改用 JSON 格式仍可能有 CSRF 問題。

使用者的防禦#

CSRF 成立前提是使用者在被攻擊的網頁處於已登入狀態,攻擊者才能拿這登入狀態做出行為,所以使用者能做的防禦就是在每次使用網站後就登出,避免 CSRF。不過使用者能做的有限,真的該防禦和處理的是伺服器。

伺服器的防禦#

CSRF 的 CS 是指 cross-site,cross-site 的攻擊代表可在任何一個網站下發動攻擊,因此防禦方向是「如何擋掉別的來源發的 request?」。

CSRF 攻擊的 request 和使用者本人發的 request 差異在於 origin 的不同,CSRF 是在任意 origin 發出,而使用者本人是在同一 origin 發出 (假設 API 和前端網站同 origin),若後端能辨別差異,就能判斷是否該相信 request。

伺服器的防禦方式 1:🔺 檢查 Referer 或 Origin header#

request 內有兩種欄位代表這 request 從哪裡來,可用來檢查是否為合法 origin,不是就拒絕:

這方式要注意的有幾點:

由上可知,檢查refererorigin 不是完善解法。

伺服器的防禦方式 2:✅ 加上圖形驗證碼或簡訊驗證碼#

以簡訊驗證碼、圖形驗證碼作為第二道檢查,因攻擊者不知驗證碼內容,無法執行攻擊。缺點是影響使用者體驗,過多檢查手續會讓使用者感到煩躁。因此這方式較適合重要操作,重要操作如:銀行轉帳或修改密碼。

雙重驗證之前在 [Security] XSS 的多道防線:Sanitization、CSP、降低影響範圍 介紹 XSS 防禦方式時也提過,收簡訊驗證碼或 email 這類雙重驗證可防禦兩種攻擊:

伺服器的防禦方式 3:✅ 加上 CSRF token#

防止 CSRF 只要確保有些資訊「只有網站自己知道」,而如何「只有網站自己知道」?

在 form 加入隱藏欄位 csrf_token,裡面的值伺服器隨機產生,這個值每次表單操作都產生新的,且會存在伺服器的 session 資料內,程式碼範例如下。

<form action="https://small-min.blog.com/delete" method="POST">
<input type="hidden" name="id" value="3"/>
<input type="hidden" name="csrf_token" value="fj1iro2jro12ijoi1"/>
<input type="submit" value="刪除文章"/>
</form>

csrf_token 的用途是當使用者送出表單後,伺服器會確認表單中的 csrf_token 和自己 session 存的是否相同,若相同代表是由自己網站發出的 request。

CSRF token 為何可防禦 CSRF 攻擊?因為攻擊者不知道 csrf_token 的值所以不知道該帶什麼值,伺服器檢查因而失敗,操作不會被執行。

CSRF token 防禦可運作的前提是需要伺服器的 state,CSRF token 須保存在伺服器,而 Double Submit Cookie 不需伺服器儲存東西。

運作方式如下:

  1. 伺服器產一組隨機 token 並加在 form 上
  2. token 不需記錄在伺服器 session,而是設一個 csrf_token 的 cookie,由 cookie 儲存同一 token
Set-Cookie: csrf_token=fj1iro2jro12ijoi1
<form action="https://small-min.blog.com/delete" method="POST">
<input type="hidden" name="id" value="3"/>
<input type="hidden" name="csrf_token" value="fj1iro2jro12ijoi1"/>
<input type="submit" value="刪除文章"/>
</form>
  1. 使用者送出時,伺服器比對 cookie 內的 csrf_token 與 form 內的 csrf_token 是否相同,相同就能確認是網站本身發的 request

Double Submit Cookie 為何可防禦 CSRF 攻擊?因為攻擊者發起攻擊時,cookie 內的 csrf_token 會一起送到 server,但表單內的 csrf_token 不會送出。攻擊者在別的網站拿不到目標網站 cookie、看不到表單內 csrf_token,沒有方式可拿到伺服器設定的 csrf_token,因此送出時表單跟 cookie 中的 csrf_token 會不符而被阻擋。

純前端的 Double Submit Cookie

Single Page Application 如何拿到 CSRF token?不能由伺服器在每次新頁面請求時塞新 token,難道要伺服器再提供 API 嗎?好像怪怪的(這樣攻擊者就可以打 API 拿 token 了)。

SPA 的 CSRF token 可參考 Double Submit Cookie 精神,由前端產 CSRF token 並放入 form 和 cookie 中,不須和伺服器 API 互動。

為何前端產 CSRF token 也可以?因為 CSRF token 的目的是「不讓攻擊者猜出」,由哪端產出都可,只要確保不被猜出。Double Submit Cookie 的核心概念是「攻擊者無法讀寫目標網站的 cookie,所以 request 中的 token 會跟 cookie 內的不同」,只要滿足此概念,就可阻擋攻擊。

其他解法#

CSRF 成立前提是瀏覽器請求時自動帶上 cookie,且這 cookie 是用來身分驗證的,因此如果不用 cookie 做身分驗證,就沒 CSRF 問題。

在現在前後端分離的架構下,許多網站更傾向用 JWT 搭配 HTTP header 做身分驗證,而非使用 cookie-based 身分驗證。JWT 身分驗證運作方式是將驗證身分 token 存在瀏覽器 localStorage,向後端發 request 時放在 Authorization header。

GET /me HTTP/1.1
Host: api.monica.tw
Authorization: Bearer {JWT_TOKEN}

JWT 驗證的優缺點如下:

加上 custom header + CORS 正確設置#

CSRF 攻擊送出請求的方式如表單或圖片,這類請求無法帶上 HTTP header,因此前端打 API 時若帶上 X-Version: web header,後端可依據是否有這 header 來判斷請求合法性。

不過如果 CORS 沒設定好還是會被 CSRF 攻擊,攻擊者可用 fetch 發請求並帶 header 如下:

fetch(target, {
method: 'POST',
headers: {
'X-Version': 'web'
}
})

帶有自訂 header 屬於非簡單請求,要通過 preflight request 檢查才會發正式 request,若 CORS 設置有正確檢查請求來源,就沒問題。

因此「加上 custom header」的防禦方式需配合 CORS 的正確設置。

實際案例#

CSRF 實際案例例如 2022 年 Google Cloud Shell 的 CSRF 漏洞 和 2023 年 Ermetic 資安公司發現的 Azure web service 漏洞,書中有介紹這裡就不細講。

小結#

選擇資安修復方式時,也要注意對其他漏洞的影響,防禦方式的取捨如:

整體來說,「加上 CSRF token」是防禦 CSRF 攻擊較好、普遍的方式,另外也可將上述防禦方式混用,建立多層防禦提升安全性。


Reference:#

如有任何問題歡迎聯絡、不吝指教✍