
前言#
承接上篇 [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 炸彈),示意圖如下:

接著以刪除文章功能來介紹 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(跨站請求偽造)就像是這個案例,小明在心理測驗網站(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,不是就拒絕:
- request 的 header 內的
referer - request 的 origin
header
這方式要注意的有幾點:
- 有些狀況可能不會帶 referer 或 origin,會沒東西檢查
- 有些使用者可能會關閉帶 referer 的功能,可能會拒絕真的使用者發出的 request
- 判定是否為合法 origin 的程式碼必須保證沒有 bug
由上可知,檢查referer 或 origin 不是完善解法。
伺服器的防禦方式 2:✅ 加上圖形驗證碼或簡訊驗證碼#
以簡訊驗證碼、圖形驗證碼作為第二道檢查,因攻擊者不知驗證碼內容,無法執行攻擊。缺點是影響使用者體驗,過多檢查手續會讓使用者感到煩躁。因此這方式較適合重要操作,重要操作如:銀行轉帳或修改密碼。
雙重驗證之前在 [Security] XSS 的多道防線:Sanitization、CSP、降低影響範圍 介紹 XSS 防禦方式時也提過,收簡訊驗證碼或 email 這類雙重驗證可防禦兩種攻擊:
- CSRF
- XSS
即使攻擊者可在頁面執行程式碼,但無法用手機收驗證碼,就無法進行後續操作
伺服器的防禦方式 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 的值所以不知道該帶什麼值,伺服器檢查因而失敗,操作不會被執行。
伺服器的防禦方式 4:✅ Double Submit Cookie#
CSRF token 防禦可運作的前提是需要伺服器的 state,CSRF token 須保存在伺服器,而 Double Submit Cookie 不需伺服器儲存東西。
運作方式如下:
- 伺服器產一組隨機 token 並加在 form 上
- 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>- 使用者送出時,伺服器比對 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 內的不同」,只要滿足此概念,就可阻擋攻擊。
其他解法#
不要用 cookie 做身份驗證#
CSRF 成立前提是瀏覽器請求時自動帶上 cookie,且這 cookie 是用來身分驗證的,因此如果不用 cookie 做身分驗證,就沒 CSRF 問題。
在現在前後端分離的架構下,許多網站更傾向用 JWT 搭配 HTTP header 做身分驗證,而非使用 cookie-based 身分驗證。JWT 身分驗證運作方式是將驗證身分 token 存在瀏覽器 localStorage,向後端發 request 時放在 Authorization header。
GET /me HTTP/1.1Host: api.monica.twAuthorization: Bearer {JWT_TOKEN}JWT 驗證的優缺點如下:
- ✅ 優點:天生對 CSRF 免疫,無 CSRF 問題(因為完全沒用到 cookie)
比起防禦 CSRF,更像是技術選擇 - 🔺 缺點:以
localStorage存 token,一旦被 XSS 就可被攻擊者偷走 token
但如果以 cookie 作為驗證,可用HttpOnly讓瀏覽器無法讀取,避免 XSS 攻擊者偷走 token
加上 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 漏洞,書中有介紹這裡就不細講。
小結#
選擇資安修復方式時,也要注意對其他漏洞的影響,防禦方式的取捨如:
- 「不要用 cookie 做身份驗證」
- 優點:可解決 CSRF 問題
- 缺點:讓 XSS 能偷到 token,增加 XSS 影響範圍
- 「加上 custom header」
- 優點:可防禦 CSRF
- 缺點:若 CORS 設置有誤,防禦就無效
整體來說,「加上 CSRF token」是防禦 CSRF 攻擊較好、普遍的方式,另外也可將上述防禦方式混用,建立多層防禦提升安全性。
Reference:#
如有任何問題歡迎聯絡、不吝指教✍