Bỏ qua đến nội dung chính
CSRFsecurityauthenticationOWASPweb security

CSRF Token: Khi Nào Còn Cần Trong 2026?

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.

Xuất bản 7 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.

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

Zalo