CSRF: One Click, Account Drained
Why I Started Learning about CSRF
I was building a web service with the mindset of "security can wait." Getting features to work came first. Then one day, I received a security audit report. "Cross-Site Request Forgery (CSRF)" was highlighted in red.
My first thought: "What's this?" I'd heard of XSS, but CSRF was completely new to me. So I decided to understand it properly — better now than after getting hacked.
What Confused Me at First
When I read the definition "Cross-Site Request Forgery," honestly, I didn't get it. "Forgery? What am I forging? What's my server not validating?"
And I kept confusing it with XSS. Both are "malicious attacks" and "web vulnerabilities," but:
- XSS executes malicious scripts.
- CSRF... forges requests?
I couldn't grasp the clear difference.
The Aha Moment: "Voice Recording Fraud"
Then I heard this analogy:
"When you call your bank and say 'transfer $10,000,' they verify it's you by your voice (or ID). CSRF is like recording your voice saying 'transfer money', then playing that recording later to trick the bank. The bank thinks 'Hey, that voice sounds right' and processes the transfer."
That's when it clicked.
CSRF is making a request look like it came from me, when it didn't. The key mechanism facilitating this is Cookies. Browsers automatically attach cookies to requests, and hackers exploit this "friendly" behavior.
Attack Scenario (Step-by-Step)
- You log into your bank (
bank.com).- Browser saves
session=abc123cookie locally.
- Browser saves
- Hacker sends an email: "Free iPhone 16 Pro! Click here to claim!"
- You click the link, landing on the hacker's site (
evil.com). - Hidden code on that page:
<!-- A hidden image tag that triggers a GET request --> <img src="https://bank.com/transfer?to=hacker&amount=1000000" style="display:none;" /> - Browser tries to load the "image":
- It sees a request to
bank.com. - "Oh, I have a cookie for
bank.com!" - It automatically attaches
session=abc123cookie. (Browser's default behavior)
- It sees a request to
- Bank server receives the request:
- "Valid session cookie found? Check."
- "Transfer approved."
You did nothing but click a link, yet money left your account.
Why Is This Possible? (Deep Dive)
Browser's "Helpful" Auto Cookie Sending
Browsers send cookies automatically for convenience, not security.
If they didn't, you'd have to manually enter "what was my session number again?" on every single click on bank.com.
The problem: Browsers can't distinguish between a request you manually triggered (clicking "Send") and one that hacker code triggered (loading an image).
Whether you:
- Click the "Transfer" button on the bank site
- Or the hacker site's script triggers the request
To the browser, both are requests to bank.com, so it attaches cookies to both. It acts like a dumb courier delivering envelopes without asking who wrote the letter inside.
GET vs POST: Both Are Vulnerable
A common beginner mistake:
"Important actions like transfers should use POST. Isn't that safe?"
No. CSRF can forge POST requests too with a simple HTML form and JavaScript.
<!-- Hidden form on evil.com -->
<form action="https://bank.com/transfer" method="POST" id="hackForm">
<input type="hidden" name="to" value="hacker" />
<input type="hidden" name="amount" value="1000000" />
</form>
<script>
// Auto-submit as soon as page loads
document.getElementById('hackForm').submit();
</script>
As soon as the page loads, this form auto-submits. Cookies are attached. The server sees it as a legitimate POST request from your logged-in session.
CORS Doesn't Prevent CSRF
Another common misconception: "If I set up CORS properly, I'm protected from CSRF."
No. CORS (Cross-Origin Resource Sharing) restricts reading the response, not sending the request. The hacker doesn't need to read the response ("Transfer Successful"). They just need the transfer request to reach the server. By the time the server processes it and decides whether to share the response (CORS check), the money is already gone.
How to Prevent CSRF (The Fix)
1. CSRF Token (Anti-CSRF Token) - The Standard Solution
I implemented CSRF Tokens in my service. This is the gold standard.
How it works:
- When a user requests a form page, the server generates a random token (e.g.,
token=xyz789) - Stores the token in both a hidden form field (or meta tag) and the server session.
- When the user submits the form, the token is sent along.
- Server checks if the "token from form" matches "token in session".
Why it stops hackers:
Hackers use your cookies (which are sent automatically), but they can't guess the random token.
Because the token is embedded in the HTML body of bank.com, and hackers can't read your browser screen (HTML).
The Same-Origin Policy (SOP) prevents the hacker's JavaScript (on evil.com) from reading the DOM of bank.com.
Real Code Example (Express.js)
// 1. Server generates token and renders page
app.get('/transfer', (req, res) => {
const csrfToken = generateRandomToken();
req.session.csrfToken = csrfToken; // Save to session
res.render('transfer', { csrfToken }); // Embed in HTML
});
// HTML form
// <form method="POST" action="/transfer">
// <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
// ...
// </form>
// 2. Server validates on submit
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('CSRF detected! Token mismatch.');
}
// Proceed with transfer
});
2. SameSite Cookie Attribute
Modern browsers support the SameSite cookie attribute, which is a game-changer.
res.cookie('session', 'abc123', {
httpOnly: true,
sameSite: 'Lax', // or 'Strict'
secure: true
});
The three SameSite options:
- Strict: Most conservative. Won't send cookies with ANY request from external sites. Downside: clicking a
bank.comlink from an email logs you out. (Poor UX because user has to login again.) - Lax (Default since Chrome 80): The sweet spot. Sends cookies for safe methods (GET — link clicks) but blocks them for unsafe methods (POST, PUT, DELETE). This stops most CSRF attacks while keeping links working.
- None: Wide open. Required when you need third-party cookie sharing (e.g., embedded widgets). Must use
Secureflag (HTTPS only).
I use sameSite: 'Lax' as my default. It covers most CSRF vectors without hurting user experience.
3. Double Submit Cookie Pattern (Stateless Defense)
"What if storing sessions on the server is too expensive or impossible (Serverless)?"
This pattern uses cookies twice — no server-side session state needed:
- When the user logs in, the server sets a random token as a cookie (
csrf_token). - The frontend reads this cookie value and includes it in a custom header (
X-CSRF-Token) with every request. - The server compares "cookie value == header value".
Why it works: Hackers can make the browser attach (send) the bank.com cookie, but they cannot read its value using JavaScript (due to SOP). So the hacker can't inject the matching value into the custom header.
If the header is missing or doesn't match the cookie, block the request.
This pattern is especially useful in stateless architectures like JWT-based authentication where the server doesn't manage sessions.
CSRF vs XSS: The Confusing Siblings
The two attacks beginners most frequently mix up.
| Aspect | CSRF (Request Forgery) | XSS (Cross-Site Scripting) |
|---|---|---|
| Goal | Execute unwanted actions (transfers, password changes) | Steal information (cookies, tokens) or run malicious code |
| Who acts | Victim executes (tricked by hacker) | Hacker's script executes directly |
| Script injection | No script injected into victim's browser | Hacker's script runs inside victim's browser |
| Defense | CSRF Token, SameSite Cookie | Input sanitization, CSP (Content Security Policy) |
| Analogy | Playing a recording of your voice to fool the bank | Hypnotizing the bank teller to obey commands |
Key takeaway:
- XSS: Hacking the user's browser context.
- CSRF: Exploiting the trust the server has in the user's browser.
6. How to Test for CSRF (Red Team Yourself)
"Is my site safe?" The only way to know is to try hacking it yourself. If you don't have a security team, you, the founder, must be the Red Team.
1. Simple HTML POC (Proof of Concept)
The most primitive but effective method.
- Create an
attack.htmlfile locally. - Build a hidden form targeting a critical API like
Delete AccountorChange Password. - Add
<body onload="document.forms[0].submit()">to auto-submit. - Open (double-click) this file in your browser while logged into your service (in another tab).
- If your account is deleted or password changed, you are vulnerable.
2. Professional Tools: OWASP ZAP / Burp Suite
Every developer should be friends with these tools.
- OWASP ZAP (Free): Has an 'Anti-CSRF Tokens Scanner'. It flags forms missing tokens immediately.
- Burp Suite (Free/Paid): Its "Generate CSRF PoC" feature is art. You capture a request (Intercept), right-click, and "Generate CSRF PoC". It creates the attack HTML for you. Showing this to your team is the fastest way to convince them to fix it.
7. CSRF in History (Lessons Learned)
"Do big tech companies get hacked?" Yes.
Netflix (2006)
In 2006, Netflix had a CSRF vulnerability. Hackers could add arbitrary movies to a user's "Rental Queue" via CSRF. If you visited a malicious site while logged in, your queue would fill up with strange movies you never picked. Adding movies seems harmless, but what if the attack changed the shipping address?? That was also possible.
YouTube (2008)
In the early days, almost every action on YouTube was vulnerable. The most famous was the "Add Friend" attack. A hacker could force thousands of users to add them as a friend or subscribe to their channel just by having them click a link. Rumor has it that even admin functions like "Delete Video" could be triggered via CSRF.
These incidents led frameworks (Rails 2.0, Django) to adopt CSRF protection by default.
The security features we take for granted (ctrl+c, v) are built on the blood, sweat, and hacks of our predecessors.
Mistake I Made in Production
I initially implemented CSRF Tokens but didn't attach them to AJAX requests. "Just protect form submissions," I thought.
But hackers can forge AJAX requests too:
// Hacker's script
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // Include cookies
body: JSON.stringify({ to: 'hacker', amount: 1000000 })
});
(Although CORS preflight blocks requests with custom headers, "simple requests" with Content-Type: application/x-www-form-urlencoded or text/plain bypass preflight and go straight through.)
I had to attach CSRF Tokens to ALL state-changing requests (POST, PUT, DELETE).
Key Takeaways
What I learned from understanding CSRF:
- Browsers auto-send cookies. They can't tell good intent from evil.
- Hackers can't control your browser, but they can trick it into sending requests.
- Defense is simple: One properly-implemented Token does the job.
- SameSite=Lax as default has dramatically reduced CSRF risk, but it hasn't eliminated it entirely.
To my past self who thought "I'll handle security later": Add CSRF Tokens from day one. Refactoring security into an existing codebase is far more painful.
Glossary
- CSRF Token: A one-time (or per-session) random code issued by the server to verify request authenticity.
- SameSite Cookie: An attribute that restricts cookies to "same-site" requests only. (
Laxis the default since Chrome 80). - SOP (Same-Origin Policy): The foundation of browser security. Prevents scripts from reading resources of a different origin.
- CORS (Cross-Origin Resource Sharing): A policy that relaxes SOP to allow cross-origin communication. NOT a CSRF defense!
- Double Submit Cookie: A stateless CSRF defense technique. The same token is placed in both a cookie and a header, then compared server-side.
FAQ & Common Questions
- Q: Are REST APIs vulnerable to CSRF since they're stateless?
- A: It depends on how authentication is handled. If tokens are stored in Local Storage and sent via
Authorization: Bearer ...header — safe (browsers don't auto-send headers). But if authentication relies on cookies — vulnerable, regardless of being stateless.
- A: It depends on how authentication is handled. If tokens are stored in Local Storage and sent via
- Q: Can you store the CSRF Token in a cookie? (Double Submit Cookie)
- A: Yes. But the CSRF token cookie must NOT be
HttpOnly(otherwise client JS can't read it to put in the header). The authentication session cookie, however, must remainHttpOnly.
- A: Yes. But the CSRF token cookie must NOT be
- Q: How do modern frameworks handle CSRF?
- A: SPAs (React/Angular/Vue) typically use JSON APIs with header-based tokens, making them relatively CSRF-safe. Traditional SSR frameworks have built-in CSRF protection: Django has
{% csrf_token %}, Spring hasCsrfFilter, Rails hasprotect_from_forgery. These insert tokens into forms automatically.
- A: SPAs (React/Angular/Vue) typically use JSON APIs with header-based tokens, making them relatively CSRF-safe. Traditional SSR frameworks have built-in CSRF protection: Django has