深入淺出 CSRF 跨站請求偽造攻擊

我們每天都在打造各式各樣的 Web 應用,從電商網站到社群平台。專注於功能、效能和使用者體驗,但常常忽略一個潛伏在暗處的威脅——CSRF (Cross-Site Request Forgery)

你可能聽過這個名詞,但真的了解它是如何運作的,以及它會帶來多大的危害嗎?這篇文章將用一個簡單的故事,搞懂 CSRF 是什麼、它想解決什麼問題,以及如何有效地防禦它。

一個關於「代簽」的故事:什麼是 CSRF?

想像一下,你是一位公司大老闆,在你的銀行網站(my-bank.com)上登入了,瀏覽器就像一位忠誠的秘書,手上拿著你的「身份憑證」(Session Cookie),可以幫你處理各種事務。

這時,一位心懷不軌的攻擊者寄了一封有趣的 Email 給你,標題是「快來看超可愛的貓咪動圖!」。你好奇地點開了連結,進入了一個看似無害的網站(evil-cat-gifs.com)。

你不知道的是,這個網站的 HTML 裡藏了一段惡意的程式碼,例如一個看不見的表單:

<!-- evil-cat-gifs.com 的惡意程式碼 -->
<body onload="document.forms[0].submit()">
  <form action="https://my-bank.com/api/transfer" method="POST" style="display:none;">
    <input type="hidden" name="to" value="Mallory" />
    <input type="hidden" name="amount" value="100000" />
  </form>
</body>

當你的瀏覽器載入這個頁面時,onload 事件觸發了 JavaScript,自動提交了這個隱藏的表單。

接下來發生的事,就是 CSRF 的核心:

  1. 請求發送: 瀏覽器向 https://my-bank.com/api/transfer 發送了一個 POST 請求。
  2. 憑證自動附加: 因為這個請求是發往 my-bank.com 的,你的瀏覽器秘書很「貼心」地把之前儲存的 my-bank.com 的身份憑證(Cookie)一起附上。
  3. 伺服器驗證: my-bank.com 的伺服器收到了這個請求。它檢查了 Cookie,發現身份憑證是合法的——「嗯,是大老闆本人沒錯!」
  4. 執行操作: 伺服器信任了這個請求,因為它通過了身份驗證。於是,它執行了轉帳操作,將 10 萬元轉給了攻擊者。

整個過程中,你只是點開了一個貓咪網站,錢就不翼而飛了。完全被蒙在鼓裡,因為這個請求是在不知情的情況下,被偽造並從你的瀏覽器發送出去的

這就是 跨站請求偽造 (Cross-Site Request Forgery)。攻擊者利用了你已登入的狀態,誘騙瀏覽器發送一個非你本意的請求。

CSRF 想要解決的核心問題是什麼?

從上面的故事我們可以總結出 CSRF 攻擊成功的兩個關鍵因素:

  1. 攻擊利用了瀏覽器在發送跨站請求時,會自動攜帶目標網站 Cookie 的特性。
  2. 受害網站的伺服器無法區分一個請求是來自於使用者在自家網站上的真實點擊,還是來自於第三方惡意網站的偽造請求。

因此,CSRF 防禦機制想要解決的核心問題是:如何驗證一個請求的「意圖」而非僅僅是「身份」?

伺服器只驗證 Cookie,等於只問:「你是誰?」。答案是:「我是已登入的使用者」。這沒錯,但伺服器還需要問一個更重要的問題:「這個操作真的是你本人想要執行的嗎?

CSRF 防禦的目標,就是讓伺服器有能力驗證後者,確保每一個會改變狀態的請求(如新增、修改、刪除)都是源自於使用者在我們自己網站上的真實意圖。

如何防禦 CSRF?——給請求一個「暗號」

既然問題在於伺服器無法驗證請求的來源與意圖,那我們就給它一個方法來驗證。以下是目前最主流且有效的防禦策略。

1. Anti-CSRF Token (同步權杖模式)

這是最經典也最可靠的防禦方式。

原理:

  1. 伺服器生成 Token: 當使用者登入或訪問一個包含表單的頁面時,伺服器會生成一個獨一無二、無法被預測的隨機字串,稱為「CSRF Token」。伺服器會將這個 Token 儲存在伺服器端的 Session 中。
  2. 前端嵌入 Token: 伺服器將這個 Token 傳遞給前端,前端在渲染頁面時,將它作為一個隱藏欄位嵌入到表單中。
    <form action="/update-profile" method="POST">
      <input type="text" name="email" value="user@example.com">
      <!-- 嵌入由伺服器給予的 Token -->
      <input type="hidden" name="csrf_token" value="aBcDeFgHiJkLmNoPqRsTuVwXyZ123456">
      <button type="submit">更新資料</button>
    </form>
    
  3. 伺服器驗證 Token: 當使用者提交表單時,這個 csrf_token 會一起被送到伺服器。伺服器在處理請求前,會比較「表單送來的 Token」和「儲存在 Session 中的 Token」是否一致。

為什麼有效?

攻擊者在 evil-cat-gifs.com 上無法得知這個 csrf_token 的值。因為瀏覽器的同源政策 (Same-Origin Policy) 會阻止惡意網站的腳本讀取 my-bank.com 的頁面內容,自然也就偷不到這個 Token。沒有正確的 Token,偽造的請求就會被伺服器拒絕。

這是一種更現代、從瀏覽器層面進行防禦的策略。SameSite 是 HTTP Cookie 的一個屬性,用來告訴瀏覽器在跨站請求中是否應該攜帶 Cookie。

它有三個值:

  • Strict 最嚴格。完全禁止瀏覽器在任何跨站請求中攜帶 Cookie。例如,就算你從 Google 搜尋結果點擊連結到你的網站,如果你的 Cookie 是 SameSite=Strict,你也會是登出狀態。
  • Lax (常用預設值): 較寬鬆。允許在一些安全的頂層導航(如點擊連結 <a href="...">)中攜帶 Cookie,但會阻止POSTPUTDELETE 等可能改變狀態的跨站請求中攜帶 Cookie。這恰好能防禦我們故事中那個 POST 表單的 CSRF 攻擊。
  • None 關閉 SameSite 限制。瀏覽器會在所有跨站請求中攜帶 Cookie,但必須同時設定 Secure 屬性(即只能在 HTTPS 下傳輸)。

如何使用?

在設定 Cookie 時,加上 SameSite 屬性即可。

Set-Cookie: session_id=...; SameSite=Lax; HttpOnly; Secure

SameSite 設定為 LaxStrict 是非常有效的 CSRF 防禦手段。目前主流瀏覽器已將 Lax 作為預設值,這大大降低了 CSRF 的風險。但為了兼容舊版瀏覽器和增加防禦深度,建議將 SameSite Cookie 與 Anti-CSRF Token 結合使用

3. 檢查來源 (Origin/Referer Header)

伺服器也可以檢查 HTTP 請求頭中的 OriginReferer 欄位,確保請求是從我們自己的網站域名發出的。

  • Referer:表示請求的來源頁面 URL。
  • Origin:更安全,只包含來源的域名,不含路徑。

為什麼不建議單獨使用?

  • 可靠性問題: Referer 可能因為使用者隱私設定或代理伺服器而被移除。
  • 安全性問題: 在某些舊瀏覽器或特定情況下,Referer 可能被偽造。

因此,它可以作為輔助的防禦手段,但不應作為唯一的防線。

結論

  • CSRF 是什麼? 一種攻擊,誘騙已登入使用者的瀏覽器,在他們不知情的情況下,向目標網站發送惡意的偽造請求。
  • 它想解決什麼問題? 解決伺服器無法驗證請求意圖的問題,確保狀態變更的請求確實源自使用者在我們網站上的真實操作。
  • 如何防禦?
    • 首選方案: 使用 Anti-CSRF Token,為每個請求加上只有伺服器和合法前端才知道的「暗號」。
    • 強力輔助: 設定 Cookie 的 SameSite=LaxSameSite=Strict 屬性,從瀏覽器層面阻斷惡意請求。
    • 縱深防禦: 結合上述兩者,並可考慮檢查 Origin Header,建立多層次的安全防護。

Web 安全是一個攻防不斷演進的領域。理解像 CSRF 這樣的經典漏洞,不僅能幫助我們寫出更安全的程式碼,更能培養我們「預設不信任」的安全思維。希望這篇文章能幫助你鞏固對 CSRF 的理解,並在你的下一個專案中,自信地築起堅固的防線!