CSRF: The Attack That Makes Your Browser Betray You π΅οΈ
CSRF: The Attack That Makes Your Browser Betray You π΅οΈ
Imagine this: You're logged into your company's admin dashboard. You take a quick detour to a random forum to check a thread someone DMed you. Nothing happens. You close it. Go back to work.
Two hours later, you get an alert. Your account just changed the admin email to [email protected] and disabled 2FA. You never clicked anything suspicious. You never entered any credentials.
Your browser did it for you. Cheerfully. Without asking.
Welcome to Cross-Site Request Forgery (CSRF) β the attack that weaponizes your own authenticated session against you. π±
What the Heck Is CSRF? π€
CSRF (pronounced "sea-surf" β yes, really) tricks your browser into making requests to a site where you're already logged in, without your knowledge or consent.
Here's the key insight: browsers automatically include cookies with every request to a domain. That includes your session cookies. So if you're logged into mybank.com and some evil page makes your browser send a request to mybank.com/transfer... your bank thinks it's you!
The attacker never sees your cookies. They don't need to. They just make your browser fire the request on your behalf. You're the one holding the loaded gun β they just pull your trigger. π«
The Classic Attack in 60 Seconds β‘
Your bank's transfer endpoint:
POST /transfer HTTP/1.1
Host: mybank.com
Cookie: session=abc123
amount=1000&to_account=98765&memo=rent
This is totally normal. Your bank sees your session cookie and processes the transfer. Fine.
The attacker's evil page:
<!-- evil.com/free-iphone.html -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://mybank.com/transfer" method="POST">
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="to_account" value="attacker_account" />
<input type="hidden" name="memo" value="invoice" />
</form>
<h1>Congratulations! You won a free iPhone! π</h1>
<p>Claiming your prize...</p>
</body>
</html>
When you load this page β bam. Your browser submits that form to mybank.com. Your session cookie goes along for the ride (because that's just how browsers work). The bank approves the transfer. Attacker gets $10,000. You get a GIF of confetti. π
The attacker never touched your account directly. They just made you do it.
It's Not Just Forms β GET Requests Too π¬
If your app does anything destructive via GET request, it's even worse:
<!-- Attacker just drops this in a forum post or email -->
<img src="https://yourapp.com/admin/delete-user?id=42" width="0" height="0" />
Your browser loads the "image." Your session cookie goes with it. User 42 is gone. The attacker just had to get you to open an email. No click required. π«₯
Real talk: NEVER use GET requests for state-changing operations. That's not just a CSRF thing β it's just good HTTP hygiene.
Why Can't Browsers Justβ¦ Not Do This? π
Browsers have a Same-Origin Policy (SOP) that prevents one site from reading responses from another site. But here's the catch: SOP blocks reading, not sending.
Your browser will happily send that POST request. It just won't let the attacker's page read the response. So for the transfer example, the evil page can't see "Transfer successful!" β but the transfer still happened. The damage is done.
This is why CSRF is so insidious. The attacker doesn't need to see anything. They just need the action to execute.
Fixing CSRF: The CSRF Token π‘οΈ
The most battle-tested defense is the synchronizer token pattern (aka CSRF token).
Here's the idea: when rendering a form, your server generates a random secret token and embeds it. The server also stores this token server-side (in the session). When the form is submitted, the server checks the submitted token matches what it stored.
The attacker can't forge this token because:
- They can't read your cookies (SOP blocks it)
- They can't read your page's HTML (SOP blocks that too)
- The token is random and unpredictable
# Flask example β generating and validating CSRF tokens
import secrets
from flask import session, request, abort
def generate_csrf_token():
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return session['csrf_token']
def validate_csrf_token():
submitted = request.form.get('csrf_token')
if not submitted or submitted != session.get('csrf_token'):
abort(403) # Forbidden β likely a CSRF attempt!
# In your route:
@app.route('/transfer', methods=['POST'])
def transfer():
validate_csrf_token()
# ... process transfer safely
<!-- In your form -->
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form>
The attacker's evil page has no way to include the correct token. Request rejected. π«
Most modern frameworks handle this automatically:
- Laravel:
@csrfblade directive + VerifyCsrfToken middleware - Django:
{% csrf_token %}template tag (enabled by default) - Rails:
protect_from_forgery(on by default) - Express:
csurfmiddleware (orcsrfpackage) - ASP.NET:
[ValidateAntiForgeryToken]attribute
If you're using one of these frameworks and haven't disabled their CSRF protection... you're probably fine. But if you rolled your own auth or disabled the middleware for "convenience," read on.
The SameSite Cookie Attribute β Modern Defense πͺ
Browsers now support a SameSite cookie attribute that largely kills CSRF at the browser level:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
SameSite=Strictβ Cookie only sent on same-site requests. Maximum protection, but breaks some legitimate cross-site navigation (like clicking a link from an email).SameSite=Laxβ Cookie sent on top-level navigations (clicking links) but NOT on cross-site form POSTs or embedded requests. This is the sweet spot and is now the default in most modern browsers.SameSite=Noneβ Old behavior. Cookie sent everywhere. RequiresSecureflag.
// Express.js: Setting SameSite on your session cookie
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'lax', // Blocks CSRF POST requests
}
}));
Important caveat: SameSite only works if your app is on a single "site" (same registrable domain). If your API is on api.example.com and your frontend is on app.example.com, that's same-site. But if they're on completely different domains, SameSite won't help and you'll need proper CSRF tokens or CORS configuration.
The CSRF Security Checklist β
Before you ship:
- CSRF tokens on all state-changing forms (POST, PUT, DELETE)
- CSRF token validated server-side on every submission
-
SameSite=LaxorStricton session cookies - No destructive actions on GET endpoints
- CORS configured to only allow trusted origins
- Framework CSRF protection NOT disabled (I'm looking at you, StackOverflow copy-pasters)
- API endpoints using header-based auth (Bearer tokens) instead of cookies where possible
Bonus: If you're building a pure API consumed by a JavaScript frontend, consider ditching cookies entirely for session management. Use Authorization: Bearer <token> headers instead. JavaScript must explicitly set headers β a cross-site form attack can't do that. Problem solved at the architecture level. π§
Real-World CSRF Hits π
Netflix (2006): Researchers found they could change a user's account email via CSRF. One malicious link = full account takeover.
YouTube (2008): CSRF let attackers add videos to your favorites, subscribe to channels, and share videos β all without your knowledge. Weird but real.
ING Direct (2008): Researchers could open a new bank account on behalf of the victim. A banking app. Wide open.
The pattern: Companies ignored CSRF because it seemed "theoretical." Then researchers demonstrated billion-dollar scenarios. Then patches shipped. Learn from them β patch before the demo.
The Bottom Line π―
CSRF is one of those vulnerabilities that feels abstract until you see it work. Then it's terrifying β because your users are the ones holding the session cookies, and browsers are just doing what browsers do.
The fixes are dead simple:
- Use CSRF tokens β your framework probably does this already
- Set
SameSite=Laxon session cookies β one line of config - Never use GET for state changes β just don't
- Know what your framework defaults are β and don't turn off security features because a tutorial told you to
The scariest part about CSRF is that it doesn't require the attacker to break into anything. They just send you a link and let your browser do the work. Make sure your app hangs up the phone before it cooperates. π
Had a close call with CSRF (or worse, a hit)? Share your war story on LinkedIn β those posts help other developers take this seriously.
Want to dig deeper into web security? Check my GitHub for more security examples and checklists.
P.S. β Go check your session cookie right now. Does it have SameSite=Lax? No? You have homework. πͺβ¨