
CSRF: One Click, Account Drained
I just clicked an interesting link, and money was transferred under my name. My journey to understanding CSRF, the sneaky attack that exploits your logged-in session.

I just clicked an interesting link, and money was transferred under my name. My journey to understanding CSRF, the sneaky attack that exploits your logged-in session.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

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.
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:
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.
bank.com).
session=abc123 cookie locally.evil.com).<!-- A hidden image tag that triggers a GET request -->
<img src="https://bank.com/transfer?to=hacker&amount=1000000" style="display:none;" />
bank.com.bank.com!"session=abc123 cookie. (Browser's default behavior)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:
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.
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.
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.
I implemented CSRF Tokens in my service. This is the gold standard.
How it works:
token=xyz789)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.
// 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
});
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:
bank.com link from an email logs you out. (Poor UX because user has to login again.)Secure flag (HTTPS only).I use sameSite: 'Lax' as my default. It covers most CSRF vectors without hurting user experience.
"What if storing sessions on the server is too expensive or impossible (Serverless)?"
This pattern uses cookies twice — no server-side session state needed:
csrf_token).X-CSRF-Token) with every request.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.
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:
"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.
The most primitive but effective method.
attack.html file locally.Delete Account or Change Password.<body onload="document.forms[0].submit()"> to auto-submit.Every developer should be friends with these tools.
"Do big tech companies get hacked?" Yes.
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.
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.
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).
What I learned from understanding CSRF:
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.
Lax is the default since Chrome 80).Authorization: Bearer ... header — safe (browsers don't auto-send headers). But if authentication relies on cookies — vulnerable, regardless of being stateless.HttpOnly (otherwise client JS can't read it to put in the header). The authentication session cookie, however, must remain HttpOnly.{% csrf_token %}, Spring has CsrfFilter, Rails has protect_from_forgery. These insert tokens into forms automatically.