0x55aa
Back to Blog

CSRF Attacks: When Your Browser Becomes a Weapon Against You 🔫🌐

|
7 min read

CSRF Attacks: When Your Browser Becomes a Weapon Against You 🔫🌐

Picture this: You're sipping coffee, logged into your company's admin panel, and you open a Slack link your coworker sent. It's a funny meme. You laugh. You close it.

The next morning, your boss calls: "Why did you delete 3,000 user accounts last night?"

You didn't. But your browser did. That's Cross-Site Request Forgery (CSRF) — the attack where your session does the hacking for you while you're looking at cat pictures. 🐱💀

What Is CSRF and Why Should You Lose Sleep Over It

Your browser is extremely helpful. Too helpful, actually. Every request it sends to a server automatically includes cookies — including your precious session tokens. Hackers figured out that if they can trick YOUR browser into making a request to a site you're logged into, the server has no idea it wasn't really you.

The anatomy of a CSRF attack:

  1. You log into mybank.com — session cookie is set ✅
  2. You visit evil-memes.com (in another tab) 😂
  3. evil-memes.com silently loads an image: <img src="https://mybank.com/transfer?to=hacker&amount=5000">
  4. Your browser fetches that "image" — with your session cookie attached 🍪
  5. mybank.com sees a valid session and executes the transfer 💸
  6. The "image" returns a 404 — you never noticed anything

The meme was free. Your $5,000 wasn't.

A Real-World Demo That'll Haunt You

Here's an actual CSRF payload that a malicious page could embed. It auto-submits the moment the page loads:

<!-- evil-memes.com/index.html -->
<!-- This form submits instantly, invisibly -->
<form id="csrf-attack" action="https://yourapp.com/api/admin/delete-users" method="POST" style="display:none">
  <input name="confirm" value="true">
  <input name="scope" value="all">
</form>

<script>
  // Fires the moment the page loads — user sees nothing
  document.getElementById('csrf-attack').submit();
</script>

If yourapp.com has no CSRF protection, every logged-in admin who visits this page just wiped their user table. The "hacker" didn't need your password, your OTP, or anything — just your active session cookie.

No phishing email. No malware. Just a rogue form submit.

The Fix #1: CSRF Tokens (The Classic Bodyguard)

The most battle-tested defense is a CSRF token — a secret, random, session-bound value that the server generates and expects back with every state-changing request.

// PHP example — generating and validating CSRF tokens
session_start();

// On page load: generate a token and embed it in the form
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// In your HTML form
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';

// On form submission: validate the token
function validate_csrf_token(string $submitted_token): bool {
    if (empty($_SESSION['csrf_token'])) {
        return false;
    }
    // hash_equals prevents timing attacks — don't use ===
    return hash_equals($_SESSION['csrf_token'], $submitted_token);
}

// In your controller
if (!validate_csrf_token($_POST['csrf_token'] ?? '')) {
    http_response_code(403);
    die('CSRF validation failed');
}

// Now safe to process the request
processFormData($_POST);

The evil evil-memes.com page can't read your CSRF token — same-origin policy blocks cross-origin JavaScript from reading cookies or responses. Without the token, the server rejects the request.

Laravel, Django, Rails, Spring — all major frameworks handle this automatically. If you're using a framework and you've disabled the built-in CSRF protection, stop what you're doing right now and re-enable it. 🛑

The Fix #2: SameSite Cookies (The Modern Shield)

The browser itself now offers a powerful defense: the SameSite cookie attribute. It tells the browser "don't send this cookie on cross-site requests."

// Node.js / Express — setting a secure session cookie
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,         // No JS access to cookie
    secure: true,           // HTTPS only
    sameSite: 'strict',     // NEVER sent on cross-site requests
    maxAge: 1000 * 60 * 60  // 1 hour expiry
  }
}));

Here's what the SameSite values actually mean:

Value Behavior Use When
strict Cookie NEVER sent cross-site Admin panels, banking
lax Sent on top-level GET navigation only Most apps (the safe default)
none Always sent (old behavior) Only for legitimate cross-site embeds

SameSite=strict is the nuclear option — the cookie isn't sent even when a user clicks a link from Gmail to your site. For banking or admin tools, that's the right call. For everything else, lax blocks CSRF while not breaking normal navigation.

The catch: SameSite requires HTTPS for none, and very old browsers don't support it. Combine it with CSRF tokens for defense-in-depth — one layer can fail, two rarely do.

The Sneaky CSRF Variants You Didn't Know Existed

JSON CSRF: Developers think "JSON APIs are CSRF-safe because forms can't send Content-Type: application/json." Mostly true — but:

// Attacker's trick: XMLHttpRequest with text/plain
fetch('https://yourapi.com/delete-account', {
  method: 'POST',
  body: '{"confirm": true}',  // Sends as text/plain, not JSON
  credentials: 'include'       // Sends your cookies!
});

If your API parses the body regardless of Content-Type, you're vulnerable. Always validate Content-Type: application/json server-side before parsing JSON endpoints.

GET-based CSRF: Ever seen an action on a GET endpoint?

GET /admin/delete?userId=123

Someone can trigger this with a single <img> tag. Never use GET for state-changing operations. This is basic REST hygiene AND a security requirement.

Quick Checklist Before You Ship

Before that next deploy, ask yourself:

  • Are CSRF tokens on every state-changing form? (POST, PUT, DELETE, PATCH)
  • Is your session cookie using SameSite=lax or strict?
  • Is httpOnly: true set on session cookies? (Blocks XSS from stealing them)
  • Is secure: true set? (HTTPS only)
  • Are your GET endpoints read-only? (No side effects on GET/HEAD)
  • For APIs: Are you validating Content-Type before parsing request bodies?
  • For SPAs: Are you sending the CSRF token in a header (X-CSRF-Token), not a cookie?

If you're using a major framework and haven't touched CSRF middleware, you're probably fine. If you've ever seen a comment in your codebase like // TODO: re-enable CSRF protection — that TODO is your highest-priority ticket right now.

The Bigger Picture

CSRF sits at a weird intersection — it's not about stealing data (that's XSS), it's about forging actions. The attacker doesn't need your password or your token. They just need you to be logged in and visit the wrong page for a fraction of a second.

The good news: modern browsers + proper SameSite cookies have made opportunistic CSRF attacks much harder. The bad news: legacy apps, custom auth systems, and "we disabled CSRF middleware because it was breaking tests" decisions create a long tail of vulnerable systems.

Test your own apps with OWASP ZAP or Burp Suite. Search your codebase for csrf_exempt, @csrf_exempt, VerifyCsrfToken in middleware exclusions — those are the landmines. Every exception to CSRF protection is a door left unlocked.

Your browser trusts the sites you're logged into. Make sure those sites deserve that trust.


Enjoyed this security deep-dive? Connect on LinkedIn — I post about web security, backend dev, and the occasional production incident horror story.

Want to see more secure code in the wild? Check my GitHub for projects where security isn't an afterthought.

P.S. — Go check your session cookie settings RIGHT NOW. I'll wait. SameSite=none without a good reason is a red flag. 🚩

P.P.S. — If your framework's CSRF protection is disabled "temporarily," that temporary fix is now a permanent vulnerability. You know who you are. 😅

Thanks for reading!

Back to all posts