0x55aa
Back to Blog

CSRF: The Attack That Makes Your Users Hack Themselves ðŸŠĪ

|
7 min read

CSRF: The Attack That Makes Your Users Hack Themselves ðŸŠĪ

Imagine you're logged into your bank. You check your email, click a link from a "totally legitimate" newsletter, and — without typing a single thing — you just transferred $5,000 to someone you've never met. You didn't do it. But your browser did. Gleefully. On your behalf.

Welcome to Cross-Site Request Forgery (CSRF), the attack that weaponizes trust. Not your trust in a shady website — the server's trust in you.

How CSRF Actually Works 🧠

Browsers have a quirk that attackers love: when your browser sends a request to yourbank.com, it automatically includes any cookies it has for that domain. Session cookies, auth cookies — all of it. The server sees a valid session and says, "Oh, it's you! Right this way!"

CSRF exploits this by tricking your authenticated browser into making a request you never intended.

Here's the classic attack flow:

  1. You log into yourbank.com. A session cookie is set.
  2. You visit evil-memes.com (in the same browser, different tab).
  3. That page silently loads this:
<!-- Hidden on evil-memes.com -->
<img src="https://yourbank.com/transfer?to=hacker&amount=5000" style="display:none">
  1. Your browser fetches that "image" URL — with your session cookie attached.
  2. yourbank.com sees a valid session and processes the transfer.
  3. You are now $5,000 poorer and very confused.

No phishing form. No stolen password. Just your own authenticated session, turned against you. 🎭

The POST Version (It Gets Worse) ðŸ“Ū

GET-based attacks are obvious, so most developers protect against them. But POST requests? That's where things get spicy.

<!-- The attacker embeds this on their site -->
<form id="evil-form" action="https://yourbank.com/transfer" method="POST">
  <input type="hidden" name="to" value="hacker_account">
  <input type="hidden" name="amount" value="5000">
</form>

<script>
  // Auto-submits the moment you visit the page
  document.getElementById('evil-form').submit();
</script>

The user visits the attacker's page, the form silently submits in the background, and the bank's server happily processes it — because the session cookie rode along for free.

This is why "just use POST instead of GET" is not a security strategy. ðŸĪĶ

How to Stop It: The CSRF Token Pattern ðŸ›Ąïļ

The fix is elegant: make every sensitive request prove it came from your page, not some random site.

Server-side (Node.js + Express example):

const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());
app.use(csrf({ cookie: true }));

// Inject the token into every rendered page
app.get('/transfer', (req, res) => {
  res.render('transfer', {
    csrfToken: req.csrfToken()  // Unique, unpredictable token per session
  });
});

// The middleware automatically validates it on POST
app.post('/transfer', (req, res) => {
  // If req.body._csrf doesn't match the session token → 403 Forbidden
  // If it matches → process the transfer
  processTransfer(req.body);
});

Client-side (your HTML form):

<form action="/transfer" method="POST">
  <!-- The hidden field carries the proof-of-origin token -->
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">

  <input type="text" name="to" placeholder="Recipient account">
  <input type="number" name="amount" placeholder="Amount">
  <button type="submit">Transfer</button>
</form>

What just happened?

  • The token is generated server-side and tied to your session.
  • It's embedded in your legitimate page.
  • An attacker on evil-memes.com can't read it (same-origin policy blocks cross-origin reads).
  • When the POST arrives without a valid token → rejected. When it arrives with one → trusted.

The attacker's form has no token. Request denied. Attack foiled. 🎉

Modern Defense: SameSite Cookies 🍊

If you're building something new, SameSite cookies are your best friend:

// In your session/cookie config
res.cookie('sessionId', token, {
  httpOnly: true,       // No JS access
  secure: true,         // HTTPS only
  sameSite: 'Strict',   // THE CSRF KILLER
  maxAge: 3600000
});

SameSite: Strict tells the browser: "Only send this cookie if the request originated from my own site."

Cross-site form submission? No cookie. API fetch from evil-memes.com? No cookie. The bank never sees an authenticated request. Attack dead on arrival. 💀

SameSite Value Behavior CSRF Protection
Strict Cookie never sent cross-site Full protection
Lax Sent on top-level navigations (links) Partial (blocks form POSTs)
None Always sent (old behavior) None — you're on your own

Gotcha: SameSite: Strict breaks OAuth flows and "login with Google" style redirects. Lax is the pragmatic default for most apps — it blocks form-based CSRF while keeping login flows working.

APIs and CSRF: Are You Actually Safe? ðŸĪ”

"I use a REST API with JWT in the Authorization header — I'm fine, right?"

Mostly yes. If your API only accepts tokens in the Authorization: Bearer <token> header (not cookies), CSRF doesn't apply. Attackers can't set custom headers cross-site.

But if your API falls back to cookies for authentication? You're back in CSRF territory.

The quick gut-check:

  • Auth via Authorization header only → not CSRF vulnerable
  • Auth via session cookies → add CSRF protection
  • Auth via cookies and allows header-less auth → definitely add CSRF protection

The "Double Submit Cookie" Pattern (For SPAs) ⚡

Single-page apps often can't use traditional CSRF tokens because they don't have server-rendered HTML. Here's the SPA-friendly approach:

  1. Server sets a non-httpOnly CSRF cookie (so JavaScript CAN read it).
  2. Client JavaScript reads the cookie and sends it as a custom header.
  3. Server verifies the header value matches the cookie.

An attacker's cross-site request can't set custom headers, so even if the cookie rides along, the header won't — and the check fails.

// Client-side (React/Vue/Angular)
const csrfToken = document.cookie
  .split('; ')
  .find(row => row.startsWith('csrf-token='))
  ?.split('=')[1];

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,  // Attacker can't set this cross-site!
  },
  body: JSON.stringify({ to: 'friend', amount: 50 })
});

Your CSRF Defense Checklist ✅

Before you ship:

  • Using SameSite=Lax (or Strict) on all auth cookies
  • CSRF tokens on all state-changing forms (POST, PUT, DELETE, PATCH)
  • API endpoints verify Authorization header, not just cookies
  • No sensitive actions triggered by GET requests
  • CORS configured to reject unexpected origins
  • Security headers include Referer / Origin validation as a backup layer

Real Talk: Is CSRF Still a Threat in 2026? 💎

Yes, but the landscape has shifted. Modern browsers default SameSite to Lax for new cookies, which kills most naive CSRF attacks. But:

  • Legacy apps still explicitly set SameSite=None
  • Cookie misconfiguration is extremely common
  • Some attack vectors (like login CSRF) still work even with Lax
  • It's still #5 on the OWASP API Security Top 10

The attacks got harder. The bugs didn't disappear.

Quick Wins (Do These Today) 🏃

  1. Audit your cookie flags — search your codebase for cookie settings and add sameSite: 'lax'
  2. Add csurf (or equivalent) to your Express/Laravel/Django app — 15-minute task, massive payoff
  3. Check your API authentication — make sure it's header-based, not cookie-dependent
  4. Test it yourself — open DevTools, copy your session cookie, try replaying a request from a different origin

The Bottom Line

CSRF is one of those attacks that feels impossible until you understand it, then feels obvious once you do. The browser's automatic cookie behavior — normally a convenience — becomes a liability when the server trusts it too much.

The good news: it's one of the most preventable vulnerabilities in web security. One well-placed token or one cookie flag change, and you've closed the door completely.

Your users deserve to make their own decisions about where their $5,000 goes. ðŸ›Ąïļ


Building something and want a second set of eyes on your auth flow? Connect on LinkedIn — always happy to talk security.

Found this useful? Share it with a dev who's still using GET requests for sensitive actions. You might save them from an awkward incident report. 😅

P.S. — Go check your cookies right now. If you don't see SameSite anywhere, that's your homework. 🍊🔐

Thanks for reading!

Back to all posts