0x55aa
Back to Blog

CSRF: When Your Browser Becomes a Double Agent 🕵️🔗

|
8 min read

CSRF: When Your Browser Becomes a Double Agent 🕵️🔗

Picture this: You log into your bank, check your balance, then open a new tab to look at cat memes. Totally normal behavior. But one of those "meme" pages is actually quietly firing a request to your bank's transfer endpoint — using your active session cookies. The bank sees a valid authenticated request. You see a cat with a monocle. Money is now gone.

That's Cross-Site Request Forgery (CSRF), and it's one of the most diabolically simple attacks on the web. 😈

What Even IS CSRF?

CSRF exploits one simple browser fact: when your browser makes a request, it automatically sends cookies for that domain.

You're logged into bank.com? Your browser has a session cookie. Now any website — even a shady one — can instruct your browser to make a request to bank.com, and your browser dutifully tags along your cookies.

The server sees a legit cookie. It doesn't know the request didn't come from you.

The anatomy of a CSRF attack:

  1. Victim logs into a legitimate site (bank, social media, admin panel)
  2. Victim visits attacker's malicious page (in another tab, same browser)
  3. Malicious page silently fires a request to the legitimate site
  4. Browser attaches the victim's session cookie automatically
  5. Legitimate site processes the request — because the cookie checks out
  6. Victim has no idea anything happened 💀

The Attack in Action 🎬

Here's how embarrassingly simple a CSRF attack looks. This is educational — please don't be evil:

<!-- Attacker's "cute memes" page -->
<html>
  <body>
    <!-- The user sees: a funny meme image -->
    <img src="funny-cat.jpg" alt="heh" />

    <!-- The browser does: sends a money transfer request -->
    <img
      src="https://yourbank.com/transfer?to=attacker&amount=500"
      style="display:none"
    />

    <!-- Or with a form that auto-submits on load -->
    <form
      id="csrf-form"
      action="https://yourbank.com/transfer"
      method="POST"
    >
      <input type="hidden" name="to" value="attacker_account" />
      <input type="hidden" name="amount" value="500" />
    </form>

    <script>
      // Auto-submit the moment the page loads
      document.getElementById("csrf-form").submit();
    </script>
  </body>
</html>

What the user sees: A meme page they opened for 2 seconds.

What their browser did: Submitted a money transfer form — with their valid session cookie attached — to their actual bank.

Result: The bank processed it. Because from the bank's perspective, it was a perfectly authenticated request. 🤦

Real-World Targets 😱

CSRF isn't just theoretical. Here's where it actually hurts:

Admin panels — CSRF to create a new admin user. Attacker gets persistent access, victim doesn't even notice.

Email/password change — CSRF to change the account email to attacker's address. Then they do a "forgot password" flow and take over the account.

OAuth approvals — CSRF to auto-approve a malicious app's permissions. The victim "granted access" without seeing any dialog.

DNS settings — Some router admin panels have had CSRF vulnerabilities. Attacker on local network changes DNS to point to phishing servers. Classic.

Social media actions — CSRF to post, follow, like, or even delete posts. This one was actually used to spread worms in early Twitter/Facebook days.

The damage scales with what your app lets authenticated users do. High-privilege users = high-value targets. 🎯

How to Stop It: CSRF Tokens 🛡️

The primary defense is a CSRF token — a random, unpredictable value tied to the user's session that must be present in every state-changing request.

The attacker's malicious page can't read your CSRF token (that would require JavaScript + CORS access, which browsers block). So if your server requires it, the forged request fails.

Here's how it looks in practice:

// Laravel makes this dead simple — it's built-in!
// In your Blade form, just add:
<form method="POST" action="/transfer">
    @csrf  {{-- This inserts a hidden _token field --}}
    <input name="to" value="{{ $recipient }}" />
    <input name="amount" value="{{ $amount }}" />
    <button type="submit">Transfer</button>
</form>

// Laravel's VerifyCsrfToken middleware automatically validates
// the token on every POST/PUT/PATCH/DELETE request.
// If it's missing or wrong → 419 error. Attack blocked! ✅

And for JavaScript/fetch-based requests:

// Include the CSRF token in your AJAX calls
// Laravel puts it in a meta tag — grab it like this:
const token = document
  .querySelector('meta[name="csrf-token"]')
  .getAttribute("content");

// Then attach it to your fetch request header
fetch("/api/transfer", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-TOKEN": token, // 👈 This is what stops the attack
  },
  body: JSON.stringify({ to: "friend", amount: 50 }),
});

// The attacker's site can't read this token from your cookies
// (HttpOnly + SameSite + CORS prevents that)
// So their forged request arrives without it → rejected!

The magic: even if an attacker fires a POST request to your server, they can't include a valid CSRF token they've never read. Request rejected. Attack dead. 🎉

The SameSite Cookie Superpower 🍪

Modern browsers give us a second line of defense: the SameSite cookie attribute.

Set-Cookie: session=abc123;
  HttpOnly;
  Secure;
  SameSite=Strict

What SameSite does:

  • Strict — Cookie is NEVER sent on cross-site requests. If you navigate from evil.com to bank.com, no cookie. Maximum protection, slightly annoying UX.
  • Lax — Cookie sent on top-level navigations (clicking a link) but NOT on background requests (images, iframes, AJAX). Good balance for most apps.
  • None — Always sent. Old behavior. Requires Secure flag. Don't use unless you have a specific reason.

SameSite=Lax is now the default in modern browsers (Chrome, Firefox, Edge). This has quietly killed a huge chunk of CSRF attacks in the wild. But don't rely on it alone — browser defaults can change, and old browsers exist.

The full cookie setup for 2026:

// PHP - set a hardened session cookie
session_set_cookie_params([
    'lifetime' => 3600,
    'secure'   => true,      // HTTPS only
    'httponly' => true,      // No JavaScript access
    'samesite' => 'Lax',    // Block cross-site requests
]);
session_start();

The Defense Checklist ✅

Lock down CSRF in your app:

  • Use CSRF tokens on every state-changing form/endpoint (POST, PUT, PATCH, DELETE)
  • Set SameSite=Lax or Strict on session cookies
  • Set HttpOnly and Secure on session cookies
  • For APIs: validate the Origin or Referer header as a secondary check
  • Never use GET requests for state changes (deleting, transferring, updating) — GET should be read-only
  • Re-authenticate for high-risk actions (password changes, money transfers, MFA changes)

Common Mistakes That Get People Owned 🤦

Mistake #1: "My app uses JSON, so I'm safe!"

CSRF can send JSON via fetch. And some older endpoints accept text/plain forms that get parsed as JSON. You're not automatically safe just because you use Content-Type: application/json.

Mistake #2: "I check the Referer header — that's enough!"

Referer is missing on HTTPS→HTTP redirects, can be stripped by privacy settings, and can be spoofed in some scenarios. Use it as a secondary check, not the only one.

Mistake #3: "My API is stateless with JWTs, CSRF doesn't apply!"

If your JWT is stored in a localStorage, it's NOT automatically attached by the browser — so CSRF doesn't apply there. But if you're storing JWTs in cookies, it absolutely does.

Mistake #4: Excluding CSRF protection for "convenience" on specific routes

Every state-changing endpoint needs protection. Attackers look for the one unprotected endpoint.

The Bottom Line 🎯

CSRF is elegant in its simplicity: it weaponizes the browser's most fundamental behavior — sending cookies automatically. The defense is equally elegant: just require a secret the attacker can't read.

  • Use CSRF tokens everywhere state changes
  • Set SameSite=Lax on your cookies (minimum)
  • Re-authenticate before high-stakes actions

Ten minutes of setup prevents someone from turning your users into unwitting sock puppets. It's one of those bugs that's almost embarrassing to get hit by in 2026. Don't let it be you.

Now go audit your forms. I'll wait. 🔐

Resources Worth Bookmarking 📚


Got a CSRF war story or a question about session security? Find me on LinkedIn — I'm always down to talk web security.

Want to see more security-focused projects? Check out my GitHub and see what's cooking. 🛡️

Stay paranoid out there — but in a healthy, productive way. 🕵️✨

P.S. — The next time someone sends you a "funny meme" link, maybe open it in a private window. Just saying. 😅

Thanks for reading!

Back to all posts