CSRF Explained: SameSite Cookies and CSRF Tokens

A
Author
15 Jul, 2026 1 min read 253 views

Cross-site request forgery (CSRF) tricks a logged-in user's browser into sending a request to your site from another site. Because browsers attach cookies automatically, the request arrives with the victim's session.

The attack

<!-- on evil.example -->
<form action="https://bank.example/transfer" method="POST" id="f">
  <input name="to" value="attacker"><input name="amount" value="50000">
</form>
<script>document.getElementById('f').submit()</script>

If bank.example relies only on a session cookie, the transfer goes through.

Defence 1: SameSite cookies

Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
  • Lax - cookies are sent on top-level navigation (clicking a link) but not on cross-site POSTs, iframes or fetch requests. Modern browsers use Lax by default when no attribute is set.
  • Strict - never sent cross-site, even when following a link (users arriving from an email link appear logged out).
  • None - always sent; requires Secure. Only for genuine cross-site use cases, and then you need other CSRF defences.

Defence 2: check Origin

const ALLOWED = new Set(['https://app.example.com']);

app.use((req, res, next) => {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  const origin = req.get('Origin');
  if (origin && !ALLOWED.has(origin)) return res.status(403).send('Bad origin');
  next();
});

Browsers also send Sec-Fetch-Site; rejecting cross-site on state-changing requests is a simple, strong check.

Defence 3: CSRF tokens

A random token stored in the session and included in every form or request header. An attacker's site can't read it, so it can't forge a valid request. Most frameworks (Django, Rails, Laravel) include this.

Rules that always apply

  • Never change state on GET. A GET can be triggered by an <img> tag.
  • APIs authenticated with an Authorization: Bearer header (not cookies) aren't vulnerable to classic CSRF, because browsers don't attach that header automatically.
  • A permissive CORS policy with credentials (Access-Control-Allow-Credentials: true for any origin) undoes your protections.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.