Câu hỏi CSRF token còn cần không nếu đã có SameSite cookie? Bài này phân tích thực tế, kèm code Node.js implement double submit + synchronizer pattern.
CSRF là gì?
Cross-Site Request Forgery: attacker dụ user click link/form ở evil.com, browser tự gửi cookie auth của victim đến alodev.vn → action thực hiện thay user.
<!-- evil.com -->
<form action="https://alodev.vn/api/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="1000000">
</form>
<script>document.forms[0].submit()</script>
Nếu alodev.vn dùng cookie auth thuần và không CSRF protect → tiền chuyển thành công.
SameSite cookie — defense chính 2026
Set-Cookie: session=...; SameSite=Lax; Secure; HttpOnly
| SameSite | Behavior |
|---|---|
| Strict | Cookie KHÔNG gửi cross-site dù navigate. UX có vấn đề (link share không login) |
| Lax | Cookie gửi khi top-level GET navigation; KHÔNG gửi cross-site POST/iframe |
| None | Gửi mọi cross-site (cần Secure). Dùng cho third-party widget |
Lax là default mới của Chrome — chặn 95% CSRF cổ điển. Strict an toàn nhất nhưng UX kém.
Khi nào vẫn cần CSRF token?
- SameSite=None — third-party context bắt buộc (e.g. embedded widget)
- Defense-in-depth — proxy/browser cũ không respect SameSite
- Compliance — PCI, banking yêu cầu CSRF token explicit
- GET state-changing — không nên có nhưng nếu có, SameSite không bảo vệ
Double submit cookie pattern
import crypto from 'node:crypto'
// Login: sinh CSRF token, set cookie (KHÔNG httpOnly để JS đọc được)
app.post('/auth/login', async (req, res) => {
// ... verify password
const csrfToken = crypto.randomBytes(32).toString('hex')
res.cookie('csrf', csrfToken, { sameSite: 'lax', secure: true })
res.cookie('session', sessionToken, { httpOnly: true, sameSite: 'lax', secure: true })
res.json({ ok: true })
})
// Middleware verify mọi state-changing request
function csrfProtect(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next()
const cookieToken = req.cookies.csrf
const headerToken = req.headers['x-csrf-token']
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF_TOKEN_MISMATCH' })
}
next()
}
app.use(csrfProtect)
// Frontend: đọc cookie, đặt vào header
function getCsrfToken() {
return document.cookie.split('; ').find(c => c.startsWith('csrf='))?.split('=')[1]
}
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken() ?? '',
},
body: JSON.stringify({ to, amount }),
})
Attacker không đọc được cookie victim (cross-origin) → không thể gửi header đúng.
Synchronizer token (server-side state)
// Render form với hidden CSRF token
app.get('/transfer', requireAuth, (req, res) => {
const token = crypto.randomBytes(32).toString('hex')
req.session.csrfToken = token
res.render('transfer', { csrfToken: token })
})
// Server-rendered HTML có:
// <input type="hidden" name="csrf" value="<%= csrfToken %>">
// Submit
app.post('/transfer', requireAuth, (req, res) => {
if (req.body.csrf !== req.session.csrfToken) {
return res.status(403).end()
}
// ... process transfer
})
An toàn hơn double submit nhưng cần session storage (Redis). Dùng cho app server-rendered (Rails, Django, Laravel pattern).
Bearer token — không cần CSRF
// SPA dùng Authorization header
fetch('/api/transfer', {
headers: { Authorization: `Bearer ${accessToken}` },
body: JSON.stringify(data),
})
Token store trong memory (không cookie auto-attach) → attacker site không có cách đính kèm. Đây là lý do nhiều SPA prefer Bearer + httpOnly refresh cookie.
Origin/Referer header check
// Defense thêm — không đủ một mình
function originCheck(req, res, next) {
const origin = req.headers.origin || req.headers.referer
const url = new URL(origin || '', `https://${req.headers.host}`)
if (url.origin !== `https://${req.headers.host}`) {
return res.status(403).end()
}
next()
}
Kết luận
CSRF token vẫn cần 2026 cho cookie auth — defense-in-depth với SameSite. Bearer token (JWT header) không cần. Double submit pattern đơn giản setup, đủ cho 95% app. Tham khảo JWT authentication để hiểu khi nào chọn cookie vs header auth.