CSRF 防禦完整指南
多層防禦策略與 Django 最佳實踐
目錄
⚠️ 免責聲明 本文內容僅供教育與學習用途。請勿將文中技術用於任何未經授權的系統或惡意目的。
📚 本篇重點
- 🎯 掌握 CSRF Token 的完整實作
- 🔒 理解 SameSite Cookie 的配置
- 🛡️ 學習多層防禦策略
- 💻 Django 實戰最佳實踐
閱讀時間: 約 20 分鐘 難度: ⭐⭐⭐ 中高階
1️⃣ 防禦層級架構
┌─────────────────────────────────────────────┐
│ Layer 1: 使用正確的 HTTP 方法 │
│ - GET: 只用於讀取 │
│ - POST/PUT/DELETE: 用於狀態改變 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Layer 2: CSRF Token ⭐ 核心防禦 │
│ - Synchronizer Token Pattern │
│ - Double Submit Cookie │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Layer 3: SameSite Cookie │
│ - Lax 或 Strict │
│ - 阻止跨站 Cookie 發送 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Layer 4: 驗證 Origin/Referer Header │
│ - 檢查請求來源 │
│ - 白名單驗證 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Layer 5: 額外驗證(敏感操作) │
│ - 重新輸入密碼 │
│ - OTP/2FA │
│ - CAPTCHA │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Layer 6: 監控與告警 │
│ - 記錄失敗的 CSRF 驗證 │
│ - 異常檢測 │
└─────────────────────────────────────────────┘2️⃣ Layer 1: 使用正確的 HTTP 方法
原則:GET 只用於讀取
# ❌ 錯誤:使用 GET 執行狀態改變
@app.route('/delete-account')
def delete_account():
user_id = request.args.get('user_id')
delete_user(user_id) # 危險!
return "帳號已刪除"
# URL: http://example.com/delete-account?user_id=123
# 攻擊者可以用 <img src="..."> 觸發!
# ✅ 正確:使用 POST/DELETE
@app.route('/delete-account', methods=['POST'])
def delete_account():
user_id = request.form.get('user_id')
delete_user(user_id)
return "帳號已刪除"Django 範例
# views.py
from django.views.decorators.http import require_http_methods
from django.http import HttpResponse, HttpResponseNotAllowed
# ❌ 錯誤:允許 GET
def transfer_money(request):
if request.method == 'GET':
amount = request.GET.get('amount')
to = request.GET.get('to')
# 執行轉帳 - 危險!
# ✅ 正確:只允許 POST
@require_http_methods(["POST"])
def transfer_money(request):
amount = request.POST.get('amount')
to = request.POST.get('to')
# 執行轉帳
return HttpResponse("轉帳成功")
# ✅ 更好:使用 Class-Based View
from django.views import View
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
class TransferView(View):
@method_decorator(csrf_protect)
def post(self, request):
amount = request.POST.get('amount')
to = request.POST.get('to')
# 執行轉帳
return HttpResponse("轉帳成功")
def get(self, request):
# GET 只用於顯示表單
return render(request, 'transfer_form.html')3️⃣ Layer 2: CSRF Token 實作
方法 1: Synchronizer Token Pattern (Django 預設)
原理:
1. 伺服器生成隨機 Token
2. Token 同時儲存在:
- Session (伺服器端)
- Cookie (客戶端)
3. 表單中包含 Token
4. 提交時驗證:Session Token == 表單 TokenDjango 完整配置:
# settings.py
# 1. 啟用 CSRF 中間件(預設已啟用)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # ✅ CSRF 中間件
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 2. CSRF Cookie 設定
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_AGE = 31449600 # 1 年
CSRF_COOKIE_DOMAIN = None
CSRF_COOKIE_PATH = '/'
CSRF_COOKIE_SECURE = True # 生產環境必須 True (HTTPS)
CSRF_COOKIE_HTTPONLY = False # 必須 False,讓 JavaScript 能讀取
CSRF_COOKIE_SAMESITE = 'Lax' # 或 'Strict'
# 3. CSRF 失敗處理
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'
# 4. 受信任的來源(可選)
CSRF_TRUSTED_ORIGINS = [
'https://example.com',
'https://www.example.com',
]
# 5. CSRF Header 名稱(用於 AJAX)
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'Template 使用
<!-- 方法 1:在 <form> 中使用 {% csrf_token %} (最常用) -->
<form method="post" action="/transfer/">
{% csrf_token %}
<input type="number" name="amount" placeholder="金額">
<input type="text" name="to" placeholder="收款人">
<button type="submit">轉帳</button>
</form>
<!-- 生成的 HTML -->
<form method="post" action="/transfer/">
<input type="hidden" name="csrfmiddlewaretoken"
value="8a7d9f2e3b4c5d6e7f8g9h0i1j2k3l4m5n6o7p8q">
<input type="number" name="amount" placeholder="金額">
<input type="text" name="to" placeholder="收款人">
<button type="submit">轉帳</button>
</form>AJAX 請求中使用 CSRF Token
方法 1:從 Cookie 讀取 Token
// 讀取 Cookie 中的 CSRF Token
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// 使用 fetch
fetch('/api/transfer/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken // 添加 CSRF Token
},
body: JSON.stringify({
amount: 1000,
to: 'recipient'
})
})
.then(response => response.json())
.then(data => console.log(data));
// 使用 XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/transfer/', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-CSRFToken', csrftoken); // 添加 CSRF Token
xhr.send(JSON.stringify({
amount: 1000,
to: 'recipient'
}));方法 2:從 DOM 讀取 Token
<!-- 在 template 中輸出 Token -->
<meta name="csrf-token" content="{{ csrf_token }}">// 從 meta tag 讀取
const csrftoken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('/api/transfer/', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken
},
body: JSON.stringify({amount: 1000, to: 'recipient'})
});方法 3:使用 jQuery (自動設置)
// jQuery 全局配置
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// 全局設置:所有 AJAX 請求自動包含 CSRF Token
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
// 現在所有 AJAX 請求都會自動帶上 CSRF Token
$.post('/api/transfer/', {
amount: 1000,
to: 'recipient'
}, function(data) {
console.log(data);
});方法 2: Double Submit Cookie Pattern
原理:
1. 生成隨機 Token
2. Token 同時設置在:
- Cookie
- 請求參數(表單或 Header)
3. 驗證:Cookie Token == 參數 Token優點: 不需要伺服器端 Session
Python 實作 (Flask 範例):
from flask import Flask, request, make_response
import secrets
app = Flask(__name__)
def generate_csrf_token():
"""生成 CSRF Token"""
return secrets.token_hex(32)
def verify_csrf_token():
"""驗證 CSRF Token"""
# 從 Cookie 讀取
cookie_token = request.cookies.get('csrf_token')
# 從表單或 Header 讀取
form_token = request.form.get('csrf_token') or \
request.headers.get('X-CSRF-Token')
# 驗證
if not cookie_token or not form_token:
return False
return cookie_token == form_token
@app.route('/transfer', methods=['GET', 'POST'])
def transfer():
if request.method == 'POST':
# 驗證 CSRF Token
if not verify_csrf_token():
return "CSRF Token 驗證失敗", 403
# 執行轉帳
amount = request.form.get('amount')
to = request.form.get('to')
# ...
return "轉帳成功"
# GET: 顯示表單
response = make_response(render_template('transfer.html'))
# 設置 CSRF Token Cookie
if 'csrf_token' not in request.cookies:
csrf_token = generate_csrf_token()
response.set_cookie(
'csrf_token',
csrf_token,
httponly=False, # JavaScript 需要讀取
secure=True, # HTTPS only
samesite='Lax'
)
return response<!-- transfer.html -->
<form method="post">
<!-- 從 Cookie 讀取 Token 並放入表單 -->
<input type="hidden" name="csrf_token" id="csrf_token">
<input type="number" name="amount" placeholder="金額">
<input type="text" name="to" placeholder="收款人">
<button type="submit">轉帳</button>
</form>
<script>
// 從 Cookie 讀取 Token
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// 設置到表單
document.getElementById('csrf_token').value = getCookie('csrf_token');
</script>4️⃣ Layer 3: SameSite Cookie
配置 SameSite Cookie
# Django settings.py
# Session Cookie
SESSION_COOKIE_SAMESITE = 'Lax' # 推薦
SESSION_COOKIE_SECURE = True # HTTPS only
# CSRF Cookie
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True三種模式比較
# Strict: 最嚴格
SESSION_COOKIE_SAMESITE = 'Strict'
# 行為:
# - 從 evil.com 點連結到 bank.com → Cookie 不發送
# - 需要重新登入
# 適用:銀行等高安全性網站
# Lax: 推薦(平衡安全性與用戶體驗)
SESSION_COOKIE_SAMESITE = 'Lax'
# 行為:
# - 從 evil.com 點 <a> 連結到 bank.com → Cookie 發送 ✅
# - 從 evil.com 提交 <form> 到 bank.com → Cookie 不發送 ✅
# 適用:大多數網站
# None: 允許跨站(必須配合 Secure)
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True # 必須
# 行為:
# - 所有跨站請求都發送 Cookie
# 適用:需要第三方 Cookie 的場景(嵌入式 iframe 等)測試 SameSite 配置
# views.py
from django.http import HttpResponse
def test_samesite(request):
"""測試 SameSite Cookie"""
response = HttpResponse("測試 SameSite Cookie")
# 設置測試 Cookie
response.set_cookie(
'test_strict',
'value',
samesite='Strict',
secure=True
)
response.set_cookie(
'test_lax',
'value',
samesite='Lax',
secure=True
)
response.set_cookie(
'test_none',
'value',
samesite='None',
secure=True
)
return response5️⃣ Layer 4: 驗證 Origin/Referer Header
自定義 CSRF 中間件
# middleware/csrf_middleware.py
from django.http import HttpResponseForbidden
from django.conf import settings
import re
class StrictCSRFMiddleware:
"""
嚴格的 CSRF 中間件
額外驗證 Origin 和 Referer Header
"""
def __init__(self, get_response):
self.get_response = get_response
# 允許的域名
self.allowed_origins = getattr(
settings,
'CSRF_ALLOWED_ORIGINS',
['https://example.com', 'https://www.example.com']
)
def __call__(self, request):
# 只檢查狀態改變的方法
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
if not self.verify_origin(request):
return HttpResponseForbidden('Invalid origin or referer')
response = self.get_response(request)
return response
def verify_origin(self, request):
"""驗證 Origin 或 Referer Header"""
# 1. 優先檢查 Origin Header
origin = request.META.get('HTTP_ORIGIN')
if origin:
return self.is_allowed_origin(origin)
# 2. 如果沒有 Origin,檢查 Referer
referer = request.META.get('HTTP_REFERER')
if referer:
return self.is_allowed_origin(referer)
# 3. 兩者都沒有 → 拒絕(可選,看安全需求)
# return False # 嚴格模式
return True # 寬鬆模式
def is_allowed_origin(self, url):
"""檢查 URL 是否在白名單中"""
for allowed in self.allowed_origins:
if url.startswith(allowed):
return True
return False# settings.py
MIDDLEWARE = [
# ...
'django.middleware.csrf.CsrfViewMiddleware',
'myapp.middleware.csrf_middleware.StrictCSRFMiddleware', # 添加自定義中間件
# ...
]
# 配置允許的來源
CSRF_ALLOWED_ORIGINS = [
'https://example.com',
'https://www.example.com',
'https://subdomain.example.com',
]在 View 中驗證
# views.py
from django.http import HttpResponseForbidden
from urllib.parse import urlparse
def verify_referer(request):
"""在 View 中驗證 Referer"""
referer = request.META.get('HTTP_REFERER', '')
if not referer:
return HttpResponseForbidden('Missing referer')
# 解析 Referer URL
parsed = urlparse(referer)
# 檢查域名
allowed_hosts = ['example.com', 'www.example.com']
if parsed.hostname not in allowed_hosts:
return HttpResponseForbidden('Invalid referer')
return None # 驗證通過
def sensitive_operation(request):
"""敏感操作:額外驗證 Referer"""
if request.method == 'POST':
# 驗證 Referer
error = verify_referer(request)
if error:
return error
# 執行操作
# ...
return HttpResponse("操作成功")
return render(request, 'form.html')6️⃣ Layer 5: 額外驗證
敏感操作需要重新驗證密碼
# views.py
from django.contrib.auth import authenticate
from django.contrib import messages
from django.shortcuts import render, redirect
def delete_account(request):
"""刪除帳號:需要重新輸入密碼"""
if request.method == 'POST':
password = request.POST.get('password')
# 驗證密碼
user = authenticate(
username=request.user.username,
password=password
)
if user is None:
messages.error(request, '密碼錯誤')
return render(request, 'delete_account.html')
# 刪除帳號
request.user.delete()
messages.success(request, '帳號已刪除')
return redirect('home')
return render(request, 'delete_account.html')<!-- delete_account.html -->
<form method="post">
{% csrf_token %}
<h2>⚠️ 刪除帳號</h2>
<p>此操作無法撤銷!請輸入密碼確認:</p>
<input type="password" name="password" required placeholder="請輸入密碼">
<button type="submit" style="background: red;">確認刪除帳號</button>
</form>使用 OTP/2FA
# views.py
import pyotp
from django.contrib.auth.decorators import login_required
@login_required
def transfer_money(request):
"""轉帳:需要 OTP 驗證"""
if request.method == 'POST':
amount = request.POST.get('amount')
to = request.POST.get('to')
otp = request.POST.get('otp')
# 驗證 OTP
user_secret = request.user.profile.otp_secret
totp = pyotp.TOTP(user_secret)
if not totp.verify(otp):
messages.error(request, 'OTP 驗證碼錯誤')
return render(request, 'transfer.html')
# 執行轉帳
do_transfer(request.user, amount, to)
messages.success(request, '轉帳成功')
return redirect('dashboard')
return render(request, 'transfer.html')<!-- transfer.html -->
<form method="post">
{% csrf_token %}
<h2>轉帳</h2>
<label>金額</label>
<input type="number" name="amount" required>
<label>收款人</label>
<input type="text" name="to" required>
<label>OTP 驗證碼</label>
<input type="text" name="otp" required placeholder="請輸入 6 位數驗證碼">
<button type="submit">確認轉帳</button>
</form>7️⃣ Layer 6: 監控與告警
記錄 CSRF 失敗
# middleware/csrf_logging.py
import logging
from django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger('security.csrf')
class CSRFLoggingMiddleware(MiddlewareMixin):
"""記錄 CSRF 驗證失敗"""
def process_view(self, request, view_func, view_args, view_kwargs):
# 這個方法在 CsrfViewMiddleware 之後執行
return None
def process_response(self, request, response):
# 檢查是否是 CSRF 失敗
if response.status_code == 403:
# 檢查是否是 CSRF 錯誤
if hasattr(request, 'csrf_processing_done'):
# 記錄詳細資訊
logger.warning(
'CSRF verification failed',
extra={
'user': getattr(request, 'user', None),
'ip': self.get_client_ip(request),
'path': request.path,
'method': request.method,
'referer': request.META.get('HTTP_REFERER', ''),
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
}
)
return response
def get_client_ip(self, request):
"""獲取客戶端 IP"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip# settings.py
MIDDLEWARE = [
# ...
'django.middleware.csrf.CsrfViewMiddleware',
'myapp.middleware.csrf_logging.CSRFLoggingMiddleware', # 添加
]
# 配置日誌
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': 'logs/csrf_failures.log',
'formatter': 'verbose',
},
},
'loggers': {
'security.csrf': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': False,
},
},
}自定義 CSRF 失敗頁面
# views.py
def csrf_failure(request, reason=""):
"""自定義 CSRF 失敗頁面"""
return render(request, 'csrf_failure.html', {
'reason': reason
}, status=403)<!-- templates/csrf_failure.html -->
<!DOCTYPE html>
<html>
<head>
<title>安全驗證失敗</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 100px auto;
padding: 20px;
text-align: center;
}
.error-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 30px;
}
h1 { color: #856404; }
p { color: #856404; }
.button {
background: #007bff;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 4px;
display: inline-block;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="error-box">
<h1>🔒 安全驗證失敗</h1>
<p>您的請求未通過安全驗證。</p>
<p>可能的原因:</p>
<ul style="text-align: left;">
<li>頁面已過期,請重新整理</li>
<li>瀏覽器 Cookie 被禁用</li>
<li>您可能遭受了 CSRF 攻擊</li>
</ul>
<p><strong>原因:</strong> {{ reason }}</p>
<a href="/" class="button">返回首頁</a>
</div>
</body>
</html># settings.py
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'8️⃣ API 與 AJAX 的 CSRF 防護
RESTful API 的 CSRF 考量
情境分析:
情境 1: Web 應用 AJAX 調用自己的 API
→ 需要 CSRF 保護 ✅
情境 2: 移動 App 調用 API
→ 不需要 CSRF 保護(使用 Token 認證)
情境 3: 第三方 App 調用 API
→ 不需要 CSRF 保護(使用 OAuth/API Key)方法 1: 豁免特定 API(使用 Token 認證)
# views.py
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
import json
@csrf_exempt # 豁免 CSRF 檢查
def api_endpoint(request):
"""
使用 Token 認證的 API
不需要 CSRF 保護
"""
# 驗證 API Token
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header.startswith('Bearer '):
return JsonResponse({'error': 'Missing token'}, status=401)
token = auth_header[7:] # 移除 "Bearer "
# 驗證 Token
if not verify_api_token(token):
return JsonResponse({'error': 'Invalid token'}, status=401)
# 處理請求
data = json.loads(request.body)
# ...
return JsonResponse({'status': 'success'})方法 2: 條件性 CSRF 保護
# middleware/conditional_csrf.py
from django.utils.deprecation import MiddlewareMixin
class ConditionalCSRFMiddleware(MiddlewareMixin):
"""
條件性 CSRF 保護
- Web 表單:需要 CSRF Token
- API(有 Token 認證):豁免 CSRF
"""
def process_view(self, request, view_func, view_args, view_kwargs):
# API 路徑豁免 CSRF
if request.path.startswith('/api/'):
# 檢查是否有 API Token
if 'HTTP_AUTHORIZATION' in request.META:
# 有 Token → 豁免 CSRF
setattr(request, '_dont_enforce_csrf_checks', True)
return None# settings.py
MIDDLEWARE = [
# ...
'myapp.middleware.conditional_csrf.ConditionalCSRFMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]方法 3: Django REST Framework 配置
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', # 需要 CSRF
'rest_framework.authentication.TokenAuthentication', # 不需要 CSRF
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# views.py (DRF)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
class TransferAPI(APIView):
"""
使用 Token 認證的 API
DRF 會自動處理 CSRF
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
amount = request.data.get('amount')
to = request.data.get('to')
# 執行轉帳
# ...
return Response({'status': 'success'})9️⃣ 完整的 Django 項目範例
讓我們整合所有最佳實踐:
# settings.py
"""
Django CSRF 完整安全配置
"""
# 基礎設定
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY')
ALLOWED_HOSTS = ['example.com', 'www.example.com']
# 中間件(順序很重要!)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # CSRF 保護
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ==================== CSRF 設定 ====================
# CSRF Cookie 設定
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_AGE = 31449600 # 1 年
CSRF_COOKIE_SECURE = True # HTTPS only
CSRF_COOKIE_HTTPONLY = False # JavaScript 需要讀取
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_USE_SESSIONS = False # False = 使用 Cookie, True = 使用 Session
# CSRF 受信任來源
CSRF_TRUSTED_ORIGINS = [
'https://example.com',
'https://www.example.com',
]
# CSRF Header 名稱(AJAX 使用)
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
# CSRF 失敗處理
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'
# ==================== Session/Cookie 設定 ====================
# Session Cookie
SESSION_COOKIE_NAME = 'sessionid'
SESSION_COOKIE_AGE = 86400 # 24 小時
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# Session 設定
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_SAVE_EVERY_REQUEST = False
# ==================== 安全 Headers ====================
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
# ==================== 日誌設定 ====================
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {name} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'logs/security.log',
'maxBytes': 1024 * 1024 * 10, # 10 MB
'backupCount': 5,
'formatter': 'verbose',
},
},
'loggers': {
'security': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': False,
},
},
}# views.py
"""
安全的 Views 範例
"""
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.contrib import messages
from django.http import HttpResponse, JsonResponse
import json
@login_required
@require_http_methods(["GET", "POST"])
def transfer_money(request):
"""
轉帳功能
✅ 使用 POST
✅ CSRF Token 保護
✅ 需要登入
✅ 額外密碼驗證
"""
if request.method == 'POST':
amount = request.POST.get('amount')
to = request.POST.get('to')
password = request.POST.get('password')
# 額外驗證密碼
if not request.user.check_password(password):
messages.error(request, '密碼錯誤')
return render(request, 'transfer.html')
# 執行轉帳
try:
do_transfer(request.user, amount, to)
messages.success(request, f'已轉帳 ${amount} 給 {to}')
return redirect('dashboard')
except Exception as e:
messages.error(request, f'轉帳失敗: {str(e)}')
return render(request, 'transfer.html')
@login_required
@require_http_methods(["POST"])
def delete_account(request):
"""
刪除帳號
✅ 只允許 POST
✅ CSRF Token 保護
✅ 需要登入
✅ 需要重新輸入密碼
"""
password = request.POST.get('password')
if not request.user.check_password(password):
messages.error(request, '密碼錯誤')
return redirect('settings')
# 記錄日誌
import logging
logger = logging.getLogger('security')
logger.warning(f'Account deleted: {request.user.username}')
# 刪除帳號
request.user.delete()
messages.success(request, '帳號已刪除')
return redirect('home')
def csrf_failure(request, reason=""):
"""自定義 CSRF 失敗頁面"""
import logging
logger = logging.getLogger('security.csrf')
logger.warning(
f'CSRF failure: {reason}',
extra={
'user': request.user.username if request.user.is_authenticated else 'anonymous',
'ip': get_client_ip(request),
'path': request.path,
}
)
return render(request, 'csrf_failure.html', {
'reason': reason
}, status=403)
def get_client_ip(request):
"""獲取客戶端 IP"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip<!-- templates/transfer.html -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSRF Token for AJAX -->
<meta name="csrf-token" content="{{ csrf_token }}">
<title>轉帳</title>
<style>
* { box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
width: 100%;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
.messages {
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
.error {
background: #f8d7da;
color: #721c24;
}
.success {
background: #d4edda;
color: #155724;
}
</style>
</head>
<body>
<h1>💸 轉帳</h1>
{% if messages %}
{% for message in messages %}
<div class="messages {{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="amount">轉帳金額</label>
<input type="number" id="amount" name="amount" required min="1" placeholder="請輸入金額">
</div>
<div class="form-group">
<label for="to">收款帳號</label>
<input type="text" id="to" name="to" required placeholder="請輸入收款帳號">
</div>
<div class="form-group">
<label for="password">確認密碼</label>
<input type="password" id="password" name="password" required placeholder="請輸入您的密碼">
</div>
<button type="submit">確認轉帳</button>
</form>
<script>
// AJAX 轉帳範例(可選)
/*
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
const csrftoken = getCookie('csrftoken');
const formData = new FormData(this);
fetch('/transfer/', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken
},
body: formData
})
.then(response => response.json())
.then(data => {
alert(data.message);
});
});
*/
</script>
</body>
</html>🔟 安全檢查清單
開發階段
- 所有狀態改變操作使用 POST/PUT/DELETE,不使用 GET
- 所有表單包含
{% csrf_token %} - AJAX 請求包含 CSRF Token (Header 或參數)
- 設置
CSRF_COOKIE_SECURE = True(生產環境) - 設置
SESSION_COOKIE_SAMESITE = 'Lax' - 敏感操作需要額外驗證(密碼/OTP)
- API 使用 Token 認證,豁免 CSRF
配置檢查
# settings.py 檢查清單
# ✅ 中間件包含 CsrfViewMiddleware
'django.middleware.csrf.CsrfViewMiddleware' in MIDDLEWARE
# ✅ CSRF Cookie 安全設定
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = False # JavaScript 需要讀取
CSRF_COOKIE_SAMESITE = 'Lax'
# ✅ Session Cookie 安全設定
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# ✅ 配置受信任來源
CSRF_TRUSTED_ORIGINS = ['https://example.com']
# ✅ 配置失敗處理
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'測試檢查
# 1. 測試沒有 CSRF Token 的請求
curl -X POST https://example.com/transfer/ \
-d "amount=1000&to=attacker" \
-b "sessionid=..."
# 預期:403 Forbidden
# 2. 測試錯誤的 CSRF Token
curl -X POST https://example.com/transfer/ \
-d "amount=1000&to=attacker&csrfmiddlewaretoken=wrong_token" \
-b "sessionid=...;csrftoken=correct_token"
# 預期:403 Forbidden
# 3. 測試 SameSite Cookie
# 在不同域名下嘗試發送請求面試常見問題
Q1: 如何在 AJAX 請求中正確使用 CSRF Token?
參考答案:
有 3 種主要方法:
方法 1:從 Cookie 讀取(推薦)
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
fetch('/api/endpoint/', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken // 添加到 Header
},
body: JSON.stringify({...})
});方法 2:從 DOM 讀取
<!-- Template -->
<meta name="csrf-token" content="{{ csrf_token }}">const csrftoken = document.querySelector('meta[name="csrf-token"]').content;方法 3:從表單元素讀取
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;關鍵點:
- CSRF Token 必須在 HTTP Header 中(
X-CSRFToken) - 或在請求體的參數中(
csrfmiddlewaretoken) - Django 會驗證 Cookie Token == Header/參數 Token
Q2: SameSite Cookie 能完全取代 CSRF Token 嗎?
參考答案:
不能! 原因:
- 瀏覽器兼容性問題:
- IE 11 不支援 SameSite
- 舊版 Safari、Chrome 也不支援
- 不能依賴所有用戶都用新瀏覽器- 同站攻擊無法防禦:
如果攻擊者在你的網站上有 XSS 漏洞:
- SameSite 無效(因為是「同站」)
- 仍然需要 CSRF Token 保護- 子域名問題:
SameSite 基於「站點」(site),不是「源」(origin)
example.com 和 evil.example.com 被視為同站
→ 子域名攻擊仍可能成功- GET 請求仍有風險(Lax 模式):
SameSite=Lax 允許 GET 跨站請求
如果開發者錯誤地用 GET 做狀態改變:
GET /delete-account?confirm=yes
→ 仍可被攻擊!最佳實踐:
SameSite Cookie + CSRF Token = 多層防禦 ✅
SameSite:第一層防禦
CSRF Token:核心防禦
額外驗證:敏感操作的最後防線配置建議:
# 兩者都要設置!
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax'
# 仍然需要在表單中使用
{% csrf_token %}Q3: 如何處理移動 App 或第三方 API 的 CSRF 保護?
參考答案:
原則: 移動 App 和第三方 API 不需要 CSRF 保護,應使用 Token 認證。
原因:
CSRF 攻擊的前提:
1. 瀏覽器自動發送 Cookie
2. 攻擊者誘騙用戶點擊惡意連結
移動 App:
- 不使用 Cookie 認證
- 使用 Token 儲存在 App 中
- 攻擊者無法獲取 Token
→ 不需要 CSRF 保護實作方式:
1. Token 認證(推薦):
# views.py
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework.response import Response
class TransferAPI(APIView):
authentication_classes = [TokenAuthentication] # Token 認證
permission_classes = [IsAuthenticated]
# 不需要 CSRF 檢查!
def post(self, request):
amount = request.data.get('amount')
to = request.data.get('to')
# 執行轉帳
return Response({'status': 'success'})2. OAuth 2.0:
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
class TransferAPI(APIView):
authentication_classes = [OAuth2Authentication]
# 不需要 CSRF 檢查!3. JWT (JSON Web Token):
from rest_framework_simplejwt.authentication import JWTAuthentication
class TransferAPI(APIView):
authentication_classes = [JWTAuthentication]
# 不需要 CSRF 檢查!移動 App 使用方式:
// iOS (Swift)
let token = "user_api_token_here"
let url = URL(string: "https://api.example.com/transfer")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["amount": 1000, "to": "recipient"]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
// 處理響應
}.resume()豁免 CSRF 的方式:
# 方法 1:使用裝飾器
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def api_endpoint(request):
# 驗證 Token
# 處理請求
pass
# 方法 2:在中間件中條件性豁免
# (見前面的 ConditionalCSRFMiddleware 範例)
# 方法 3:使用 DRF(自動處理)
# DRF 會自動處理不同認證方式的 CSRF安全建議:
- ✅ 使用 HTTPS(防止 Token 被竊取)
- ✅ Token 應有過期時間
- ✅ 實施 Rate Limiting
- ✅ 記錄 API 訪問日誌
- ❌ 不要在 URL 中傳遞 Token(會被記錄)
- ❌ 不要在 Cookie 中儲存 Token(會自動發送)
重點回顧
核心概念
多層防禦:
- Layer 1: 正確的 HTTP 方法
- Layer 2: CSRF Token(核心)
- Layer 3: SameSite Cookie
- Layer 4: Referer/Origin 驗證
- Layer 5: 額外驗證(敏感操作)
- Layer 6: 監控與告警
CSRF Token 實作:
- Synchronizer Token Pattern(Django 預設)
- Double Submit Cookie Pattern
- 在表單中:{% csrf_token %}
- 在 AJAX 中:X-CSRFToken Header
SameSite Cookie:
- Strict:最嚴格,所有跨站請求都阻擋
- Lax:推薦,阻擋跨站 POST/PUT/DELETE
- None:允許跨站(必須配合 Secure)
API 認證:
- 移動 App/第三方 API 使用 Token 認證
- 不需要 CSRF 保護
- 使用 HTTPS + Token 過期 + Rate Limiting
延伸閱讀
- OWASP CSRF Prevention Cheat Sheet
- Django CSRF Protection
- SameSite Cookie Explained
- Django REST Framework Authentication
🔗 系列導航
- 上一篇: 05-1 CSRF 基礎
- 下一篇: 05-3 CSRF 攻擊實例與演練
- 返回目錄: Web Security 系列
📝 本文完成日期: 2025-01-15 🔖 標籤: #WebSecurity #CSRF #Django #Python #防禦策略 #面試準備