Bỏ qua menu, vào nội dung chính

CSRF token: khi nào vẫn cần khi đã có SameSite cookie

CSRF token khi nào cần với SameSite cookie? Phân tích thực tế: cookie auth vs Bearer token, double submit, synchronizer pattern. Code Node.js.

3 phút đọc

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.

OWASP Cheat Sheet về phòng chống CSRF
OWASP Cheat Sheet về phòng chống CSRF (cheatsheetseries.owasp.org), ảnh chụp ngày 27/09/2026.

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.

html<!-- 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.

textSet-Cookie: session=...; SameSite=Lax; Secure; HttpOnly
SameSiteBehavior
StrictCookie KHÔNG gửi cross-site dù navigate. UX có vấn đề (link share không login)
LaxCookie gửi khi top-level GET navigation; KHÔNG gửi cross-site POST/iframe
NoneGửi mọi cross-site (cần Secure). Dùng cho third-party widget

Lax là default mới của Chrome - chặn phần lớn kiểu 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ệ
javascriptimport 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)
tsx// 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)

javascript// 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

javascript// 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

javascript// 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 phần lớn ứng dụng. Tham khảo JWT authentication để hiểu khi nào chọn cookie vs header auth.

Nhận bài mới

Để lại email để nhận bài mới của series Kiến thức lập trình, hoặc theo dõi bằng RSS.

RSS Kiến thức lập trình

Email chỉ dùng để gửi bài mới, xem Chính sách bảo mật.

Trao đổi về dự án

Cần tư vấn?