0x55aa
← Back to Blog

CSRF: The Attack That Tricks Your Browser Into Betraying You 🎭

8 min read

CSRF: The Attack That Tricks Your Browser Into Betraying You 🎭

Imagine you're logged in to your bank. You check your balance, pay a bill, life is good. Then you open a new tab and click a funny meme link your friend sent you.

That meme just transferred $5,000 out of your account.

No malware. No phishing page. No password stolen. Just a humble HTML form hiding in the corner of a cat meme website. Welcome to Cross-Site Request Forgery (CSRF) — where your browser becomes the weapon, and you're the one pulling the trigger. 🔫

What Is CSRF? 🤔

CSRF = an attacker tricks your browser into making a request to another site using your existing session cookies — without you knowing.

Your browser helpfully attaches cookies to every request automatically. That's how you stay logged in everywhere. But it also means:

If a malicious page can make your browser fire off an HTTP request to your bank... your bank sees it as you making the request. Because your cookie is right there!

Browsers don't check which page initiated the request. They just attach whatever cookies exist. That's the hole CSRF crawls through. 🐛

The Classic Attack (Shockingly Simple) 😈

Let's say a banking site has this endpoint:

POST /transfer
Body: to_account=12345&amount=5000

And it just checks "is this user logged in?" — which it confirms via session cookie. No other verification.

Here's all an attacker needs on their evil page:

<!-- Evil page the attacker controls -->
<html>
  <body>
    <h1>🐱 TOP 10 CATS OF 2026 🐱</h1>

    <!-- This runs silently as soon as the page loads -->
    <form id="steal" action="https://yourbank.com/transfer" method="POST">
      <input type="hidden" name="to_account" value="HACKER_ACCOUNT" />
      <input type="hidden" name="amount" value="5000" />
    </form>

    <script>
      document.getElementById('steal').submit();
    </script>
  </body>
</html>

That's it. That's the whole attack.

The victim visits the cat page. The form auto-submits. The browser includes the bank's session cookie. The bank processes the transfer. The attacker gets $5,000. The victim is still looking at cats. 😿

No user interaction needed. No JavaScript exploits. Just HTML and your browser's good-faith cookie behavior.

Why This Works (The Browser Trust Problem) 🍪

Cookies have a "same-site" problem baked into their original design:

  1. You log in to bank.com → bank stores a session cookie in your browser
  2. Browser stores: "Send this cookie with every request to bank.com"
  3. You visit evil.com → it makes your browser fire a request to bank.com
  4. Browser thinks: "Request to bank.com? Better send that cookie! 🙂"
  5. Bank sees a request with valid session cookie → "Authorized!" ✅

The browser is following the rules perfectly. The rules are just... dangerously naive.

GET Requests Are Even Worse 💀

If your site uses GET for state-changing actions (please don't), CSRF is trivially easy:

<!-- No form needed. Just an image tag. -->
<img src="https://yourbank.com/transfer?to=HACKER&amount=9999" />

The victim never clicks anything. The browser loads the "image" URL and boom — request fired, cookies included. This is why GET requests should NEVER modify state. Ever. That's not just a REST principle — it's a security requirement.

Real-World Impact 🌍

CSRF has caused real damage over the years:

  • YouTube (2008): A CSRF flaw let attackers add videos to anyone's favorites and add friends — a seemingly small thing that was actually used for massive spam campaigns
  • Netflix (2006): Attackers could change account email/password via CSRF, locking users out of their own accounts
  • Gmail: CSRF was used in combination with other attacks to redirect email filters, silently forwarding victims' mail to attackers
  • Routers: Many home routers had CSRF vulnerabilities that let any webpage change your DNS settings — silently redirecting all your traffic

CSRF is still in the OWASP Top 10. In 2026. Because developers keep building forms without CSRF protection. Don't be that developer.

How to Actually Stop CSRF 🛡️

Option 1: CSRF Tokens (The Classic Fix)

Generate a random, unpredictable token per session (or per form), embed it in every form, and verify it server-side. An attacker on evil.com can't read your session token — they can only send requests, not read responses (thanks to the Same-Origin Policy).

# Flask example
import secrets
from flask import session, request, abort

@app.before_request
def ensure_csrf_token():
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)

@app.route('/transfer', methods=['POST'])
def transfer():
    # Verify the token from the form matches the one in session
    if request.form.get('csrf_token') != session.get('csrf_token'):
        abort(403)  # Attacker's form didn't include the right token!

    # Safe to process
    do_the_transfer()
<!-- Every form gets the token as a hidden field -->
<form method="POST" action="/transfer">
  <input type="hidden" name="csrf_token" value="{{ session.csrf_token }}" />
  <input name="amount" />
  <button type="submit">Transfer</button>
</form>

The attacker's evil form doesn't know what your CSRF token is. They can't include it. Server rejects the request. Attack foiled! 🎉

Most frameworks handle this automatically:

  • Django: Built-in {% csrf_token %} template tag
  • Laravel: @csrf Blade directive
  • Rails: protect_from_forgery (on by default)
  • Express: csurf middleware (or csrf-csrf package)

Use your framework's built-in CSRF protection. Don't roll your own.

Option 2: SameSite Cookie Attribute (The Modern Fix)

This is the elegant, newer solution. You tell the browser: "Only send this cookie for requests that originate from my site."

# Setting SameSite on your session cookie
response.set_cookie(
    'session',
    value=session_token,
    httponly=True,
    secure=True,
    samesite='Strict'   # or 'Lax' for a less strict version
)

SameSite=Strict: Cookie is NEVER sent on cross-site requests. Rock solid security, but can break some legitimate cross-site flows (like following email links).

SameSite=Lax: Cookie is sent on top-level navigations (clicking a link) but NOT on form submits, iframes, or AJAX from other origins. This is the sweet spot for most apps — good protection without breaking UX.

SameSite=None: No protection. You must use this for third-party cookie scenarios (OAuth, embedded widgets), but always pair it with Secure.

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

Modern browsers default to Lax even when SameSite isn't specified — which is why CSRF is less catastrophically common today. But don't rely on browser defaults. Set it explicitly!

Option 3: Verify the Origin Header

For AJAX requests, check where the request came from:

// Express middleware
app.use((req, res, next) => {
    if (req.method === 'POST') {
        const origin = req.headers.origin || req.headers.referer;
        const allowedOrigins = ['https://yoursite.com'];

        if (!allowedOrigins.includes(origin)) {
            return res.status(403).json({ error: 'CSRF detected' });
        }
    }
    next();
});

This isn't bulletproof on its own (some browsers don't send Origin headers, and Referer can be stripped by privacy tools), but it's a solid extra layer.

The CSRF Security Checklist ✅

Before you ship that form:

  • All state-changing operations use POST (or PUT/DELETE), not GET
  • CSRF tokens on every form (or your framework's equivalent)
  • SameSite=Lax or Strict on session cookies
  • HttpOnly on session cookies (stops JavaScript from stealing them too)
  • Secure on session cookies (HTTPS only)
  • Double-check that your CSRF tokens are actually being validated server-side
  • API endpoints using Authorization: Bearer headers instead of cookies are naturally CSRF-immune ✅

A Note on API-Only Backends 🔌

If your frontend communicates via AJAX with Authorization: Bearer <token> headers instead of cookies — you're already CSRF-safe! Attackers can make cross-origin form submissions, but they can't set custom headers like Authorization. CSRF is fundamentally a cookie problem.

This is one reason modern SPAs with JWT in localStorage + bearer tokens don't need CSRF tokens. They have a different problem (XSS can steal localStorage), but that's a story for another post.

The Bottom Line 🎯

CSRF is one of those vulnerabilities that sounds complex but is embarrassingly easy to prevent:

  1. Use your framework's built-in CSRF protection — it's there for a reason
  2. Set SameSite=Lax on session cookies — free protection, zero cost
  3. Never use GET for state-changing actions — REST principles and security agree on this one
  4. Use bearer tokens for APIs — naturally CSRF-immune

The attack is elegant in its simplicity: your browser trusts cookies, attackers abuse that trust. Your defense is equally simple: make sure requests include something only the real user could have. That's all a CSRF token is.

Don't let a hidden form on a cat website drain your users' accounts. 🐱


Building secure web apps? Share your CSRF horror stories on LinkedIn — I'd love to hear what you've encountered in the wild!

More security deep-dives on GitHub — including code examples for securing Laravel, Node.js, and more.

P.S. — Go check if your session cookie has SameSite=Lax right now. Open DevTools → Application → Cookies. If it just says "None" or nothing at all... you have some work to do. 🎭✨