CSRF: Anti-CSRF Tokens và SameSite Cookies trong thực tế
Hướng dẫn chống CSRF: cơ chế tấn công, Anti-CSRF Token, synchronizer pattern, signed double-submit cookie, SameSite Strict/Lax/None, Next.js và checklist production.
ShanverseCập nhật 11 phút đọc— lượt xemĐang tải lượt xem
CSRF
Web Security
Một kịch bản CSRF điển hình
Giả sử người dùng đã đăng nhập bank.example và browser đang giữ session cookie:
Endpoint chuyển tiền nhận request:
Một website độc hại có thể tạo form trỏ tới endpoint đó và tự submit. Browser không cho attacker đọc response do Same-Origin Policy, nhưng việc gửi một số cross-site requests vẫn được phép. Nếu session cookie được gửi kèm và server chỉ kiểm tra cookie, request có thể thành công.
Điều kiện để CSRF xảy ra
Thông thường cần đủ các yếu tố:
Người dùng đang authenticated.
Browser tự đính kèm credential như cookie, HTTP authentication hoặc client certificate.
Bình luận
Endpoint thay đổi state.
Attacker có thể dự đoán hoặc tạo request hợp lệ.
Server không kiểm tra bằng chứng rằng request được khởi tạo từ UI/origin hợp lệ.
CSRF là lỗ hổng về request authenticity, không phải thiếu authentication. Session cookie có thể hoàn toàn hợp lệ nhưng ý định của request là giả mạo.
CSRF khác XSS như thế nào?
Nếu attacker có XSS cùng origin, script có thể đọc token trong DOM/API và gửi request hợp lệ. Không xem Anti-CSRF Token là giải pháp thay cho XSS prevention.
Nguyên tắc đầu tiên: GET không được thay đổi state
Các hành động như xóa tài khoản, đổi email, thanh toán hoặc cập nhật quyền không được dùng GET:
Links, images, prefetchers, crawlers và top-level navigation có thể phát GET ngoài ý muốn. Dùng POST, PUT, PATCH hoặc DELETE theo semantics phù hợp—nhưng đổi method chưa đủ để chống CSRF.
Lớp 1: Synchronizer Token Pattern
Server tạo token ngẫu nhiên, khó đoán và liên kết với session. Token được gửi tới legitimate UI, sau đó client phải gửi lại trong mỗi unsafe request.
Attacker site có thể kích hoạt cookie, nhưng không thể đọc token từ origin mục tiêu do Same-Origin Policy.
Yêu cầu của token tốt
Sinh bằng CSPRNG, entropy đủ lớn.
Gắn với session hoặc user context.
Không đặt trong URL/query string.
Không ghi vào access logs, analytics hay Referer.
So sánh server-side trước khi thực hiện action.
Rotate khi session được tạo lại hoặc security event yêu cầu.
Không dùng email, timestamp hoặc user ID làm token.
Per-session và per-request token
Per-session: đơn giản, ít lỗi UX, vẫn hiệu quả nếu token đủ mạnh.
Per-request: giảm replay window nhưng dễ làm back button, multiple tabs và concurrent requests phức tạp.
Không chọn per-request chỉ vì “nghe an toàn hơn” nếu implementation làm phát sinh bypass hoặc lỗi vận hành.
Ví dụ token server-side với Node.js
Khi tạo session:
Legitimate page có thể lấy token qua server-rendered field hoặc endpoint same-origin:
Với fetch, header custom thường rõ ràng hơn:
Server phải kiểm tra token trước side effect:
Token chỉ có giá trị khi mọi unsafe endpoints đều enforce cùng policy. Một legacy endpoint bị bỏ sót vẫn là lỗ hổng.
Lớp 2: Double-Submit Cookie
Khi server không giữ CSRF token trong session, có thể dùng double-submit pattern:
Server đặt CSRF cookie.
Client đọc giá trị cookie và gửi lại trong header/body.
Server yêu cầu cookie và request token giống nhau.
Session cookie vẫn nên là HttpOnly; riêng CSRF cookie phải đọc được bởi client JavaScript nếu client cần copy sang header.
Không dùng naive double-submit cho hệ thống nhạy cảm
Naive comparison có thể bị ảnh hưởng bởi cookie injection hoặc subdomain compromise. OWASP khuyến nghị signed double-submit cookie gắn token với session-specific value bằng HMAC.
Concept:
Server recompute HMAC từ session binding hiện tại và verify bằng constant-time comparison. Không đặt session ID plaintext trong token nếu không cần; dùng một session-bound value phù hợp.
HMAC không thay random token
HMAC bảo đảm integrity và binding. Random component giúp token không dự đoán được và tránh lặp. Secret phải được quản lý, rotate có kế hoạch và không bao giờ gửi cho client.
Lớp 3: SameSite Cookies
SameSite yêu cầu browser giới hạn cookie theo site context. “Same-site” không giống “same-origin”: scheme và registrable domain tham gia quyết định, còn subdomains có thể vẫn cùng site dù khác origin.
SameSite=Strict
Cookie chỉ được gửi trong same-site context. Đây là lựa chọn mạnh nhưng có thể làm hỏng flows từ external links, OAuth/payment redirects hoặc cross-site integrations.
SameSite=Lax
Cookie được gửi cho same-site requests và một số top-level cross-site navigations dùng safe methods. Nó chặn nhiều cross-site POST/form attacks với UX tốt hơn Strict.
Tuy nhiên:
State-changing GET vẫn nguy hiểm.
Same-site subdomain compromise có thể vượt qua site boundary.
Browser behavior và exceptional flows cần test.
Không nên dùng Lax như lớp duy nhất cho giao dịch nhạy cảm.
SameSite=None
Cookie được gửi trong cross-site contexts và Secure là bắt buộc trên browsers hiện đại. Giá trị này gần như không cung cấp CSRF protection; dùng khi iframe, federated login hoặc embedded flow thực sự cần cross-site cookie và bổ sung token/origin defenses.
Luôn set SameSite rõ ràng
Nhiều browser mặc định cookie thiếu thuộc tính thành Lax, nhưng không nên phụ thuộc vào default hoặc compatibility quirks. Cấu hình rõ Strict, Lax hoặc None và test trên supported browsers.
Cookie hardening trong Next.js
Prefix __Host- yêu cầu Secure, Path=/ và không có Domain, giúp giảm cookie shadowing/injection từ subdomain. Chỉ dùng khi deployment đáp ứng các điều kiện này.
Lớp 4: Origin và Referer Validation
Với unsafe requests, server có thể kiểm tra:
Nếu Origin có mặt, yêu cầu exact match với allow-list—không dùng endsWith("example.com"). Nếu header thiếu theo legitimate scenario, có thể fallback sang parsed Referer với policy rõ ràng.
Trong thực tế cần xử lý proxy headers, canonical public origin và flows không gửi Origin. Fail closed khi có thể; monitor trước khi enforce rộng.
Không chỉ search hostname như substring. https://app.example.com.attacker.test không phải origin hợp lệ.
Lớp 5: Fetch Metadata
Modern browsers gửi headers như:
Server có thể reject unsafe cross-site requests trước khi xử lý:
Cần allow-list legitimate integrations và có fallback cho clients không gửi Fetch Metadata. Đây là defense-in-depth, không thay token trên hệ thống có browser support đa dạng.
Custom Header và CORS
Một attacker form không thể tự thêm arbitrary X-CSRF-Token header. Cross-origin JavaScript muốn thêm header custom thường gây CORS preflight. Vì vậy, API có thể yêu cầu custom header cho mọi unsafe request.
Nhưng defense này chỉ mạnh khi CORS được cấu hình chặt:
Không reflect origin tùy ý.
Không allow credentials cho untrusted origins.
Chỉ allow methods/headers cần thiết.
Validate origin chính xác.
CORS không tự động chống CSRF. Simple form requests có thể được gửi mà không cần đọc response. Đừng nhầm Same-Origin Policy/CORS với request authenticity.
Cookie Auth và Bearer Token Auth
Cookie-based session
Browser tự gửi cookie, nên unsafe endpoints cần CSRF defense.
Authorization header
Nếu frontend tự đọc token và gắn vào:
Cross-site form không thể tự tạo header này, nên classic CSRF thường không áp dụng. Tuy nhiên:
Nếu token cũng được đọc từ cookie server-side, CSRF risk quay lại.
CORS sai có thể cho untrusted origin gửi credentialed requests.
Token trong browser storage làm tăng impact của XSS.
XSS vẫn có thể gọi API như user.
Chọn auth architecture theo threat model, không di chuyển token sang localStorage chỉ để né CSRF.
Login CSRF
CSRF không chỉ nhắm actions sau login. Login CSRF khiến nạn nhân đăng nhập vào account do attacker kiểm soát, rồi dữ liệu nạn nhân có thể bị lưu vào account đó.
Bảo vệ login endpoints bằng:
CSRF token hoặc validated transaction state.
SameSite phù hợp.
OAuth/OIDC state và PKCE đúng cách.
Regenerate session sau login để chống fixation.
OAuth state có nhiệm vụ gắn authorization response với browser transaction; không dùng một constant state.
Những implementation sai phổ biến
Chỉ kiểm tra token nếu field có mặt; thiếu token vẫn cho qua.
Token không gắn với session.
Token predictable hoặc dùng timestamp.
Đưa token vào URL và leak qua logs/history/Referer.
Chấp nhận token từ cookie mà không yêu cầu bản sao trong header/body.
So sánh bất kỳ CSRF cookie do client set mà không ký/binding.
Chỉ dùng SameSite=Lax nhưng vẫn có state-changing GET.
Đặt SameSite=None mà tưởng đã chống CSRF.
Origin allow-list bằng substring hoặc suffix sai.
Cho CORS credentials với origin động.
Bảo vệ form UI nhưng quên JSON, GraphQL, upload hoặc legacy API.
Cho phép method override biến GET thành action nguy hiểm.
Tắt framework CSRF protection để “API chạy được”.
Kiến trúc phòng thủ khuyến nghị
Ứng dụng web truyền thống, server session
Synchronizer token do framework cung cấp.
Session cookie Secure; HttpOnly; SameSite=Lax hoặc Strict.
Origin/Fetch Metadata checks.
Re-authentication cho action nhạy cảm.
SPA cùng origin với cookie session
Endpoint bootstrap trả CSRF token.
Client gửi token bằng X-CSRF-Token.
Cookie session HttpOnly; CSRF token nằm trong memory hoặc readable CSRF cookie tùy pattern.
CORS deny untrusted origins.
Stateless service cần cookie auth
Signed double-submit token gắn với session context.
SameSite rõ ràng.
Origin validation.
Secret rotation và token verification tập trung.
Cross-site embed/OAuth/payment
SameSite=None; Secure chỉ cho cookie cần thiết.
Transaction-specific state/token.
Exact redirect URI allow-list.
Verify origin/message source cho iframe postMessage.
Tách cookie scopes khi có thể.
Testing Checklist
Mọi unsafe endpoint từ chối request thiếu token.
Token sai, token của session khác và token cũ bị từ chối theo policy.
Token không xuất hiện trong URL, logs hoặc analytics.
Cross-site form POST bị chặn.
Cross-site JSON request không vượt qua CORS/token checks.
State-changing GET không tồn tại.
Method override không bypass policy.
SameSite Strict/Lax/None được test theo từng flow.
OAuth, payment và external redirect vẫn hoạt động an toàn.
Origin/Referer exact-match, không substring.
Requests thiếu Origin được xử lý theo documented fallback.
Sec-Fetch-Site: cross-site unsafe request bị chặn.
Subdomain takeover/compromise nằm trong threat model.
Login CSRF được kiểm thử.
XSS paths được kiểm thử độc lập.
Multi-tab, back button và token rotation không gây bypass.
Production Checklist
Dùng built-in framework CSRF protection nếu có.
Token sinh bằng CSPRNG và gắn với session/transaction.
Signed double-submit dùng HMAC và session binding.
Session cookie có Secure, HttpOnly, SameSite rõ ràng.
Ưu tiên __Host- cookie khi phù hợp.
Unsafe methods enforce token centrally.
Origin/Referer và Fetch Metadata là lớp bổ sung.
CORS allow-list chính xác, không dynamic reflection.
GET/HEAD/OPTIONS không có side effects.
Sensitive action có re-authentication/confirmation.
Monitoring ghi nhận CSRF rejection nhưng không log token.
Có metrics theo endpoint, origin và rejection reason.
Secret/session rotation có kế hoạch.
Legacy/mobile/non-browser clients có documented policy.
Security regression tests chạy trong CI.
Kết luận
CSRF tồn tại vì browser gửi credential đúng kỹ thuật nhưng server thiếu bằng chứng về ý định và nguồn khởi tạo. Same-Origin Policy ngăn attacker đọc dữ liệu, không ngăn mọi cách gửi request.
Phòng thủ đáng tin cậy là nhiều lớp: dùng synchronizer token hoặc signed double-submit token cho unsafe actions, cấu hình session cookie với Secure, HttpOnly và SameSite phù hợp, rồi bổ sung Origin/Referer validation, Fetch Metadata, CORS chặt và re-authentication cho giao dịch quan trọng.
SameSite giảm attack surface rất mạnh nhưng không phải lý do bỏ Anti-CSRF Token ở các ứng dụng có rủi ro cao hoặc flows phức tạp.
JWT trong SPA nên lưu ở đâu? Phân tích LocalStorage, sessionStorage, memory, HttpOnly Cookies và BFF, kèm kiến trúc khuyến nghị, code Next.js và checklist production.
Hướng dẫn cài đặt PostgreSQL trên Ubuntu chi tiết từ A-Z: cài đặt, tạo database và user, cấu hình remote access, UFW, backup, restore và xử lý lỗi thường gặp.
Hướng dẫn xử lý transaction storm trong flash sale và quyết toán lương với RabbitMQ/Kafka, rate limiting, backpressure, circuit breaker, load shedding, idempotency và capacity planning.