Press enter or click to view image in full size
You’re logged into your bank in one browser tab. Meanwhile, you’re casually browsing cat memes on a very suspicious website called CatsThatHack.com
.
Suddenly, without clicking anything, a request is secretly sent from CatsThatHack.com
to the bank that tells the bank to:
“Transfer $1000 to Hacker”
Since you’re already logged in on your browser, your bank says:
“Oh, it’s you! Sure, transferring now!”
You never approved it. You never clicked anything.
But the hacker piggybacked on your bank’s login session to authorize a transaction on your behalf 😶.
In Part 1 of this series, we’ll explore how CSRF(Cross site request forgery) works and the techniques attackers use to exploit it without the user’s knowledge.
Cross-Site Request Forgery (CSRF) is a type of attack that tricks a user’s browser into performing unwanted actions on a web application in which the user is already authenticated to.
But what is the core flaw that introduces the vulnerability ? 🤔
The 2 points below should answer this question:
Lets talk a bit about the normal operation of a web browser. When you’re logged into a site, your browser stores a session cookie so you stay authenticated across multiple requests/tabs. Anytime your browser makes a request to that site from the same domain or a different domain, it automatically includes that cookie.
If browsers didn’t work like this, the user would need to start a new session every time they opened the same site in a new tab in their browser. That would be clearly a terrible user experience!
🤷♂️️Browsers don’t differentiate between same-site and cross-site requests and send credentials(cookies) for both of the requests.
Historically, servers didn’t verify the source of incoming requests. If a request arrived with a valid session cookie, the server simply trusted it:
“Hey, this cookie checks out. This must be the real user.”
But that assumption allowed a issue to creep in .
A malicious site like CatsThatHack.com
could trick the browser into sending a request to another site like bank.com
. And the browser, doing what it’s supposed to, would attach the user’s bank.com
session cookie — because the user was already logged in. Also from the web server’s point of view, it looks like:
🤷♂️️Servers didn't know the difference between same-site and cross-site requests unless extra precautions were taken.
Alright enough of the theory and diagrams, lets see this in action!
I have created a simple and generic bank app to demonstrate the attack. Yep, my creativity definitely failed me here 🫠.
To start with, lets clone our demo bank application from here and run it. Detailed steps on how to run it is given in the README if you would like to follow along.
We have our vulnerable bank’s login page at http://insecure-bank.com/login
. The victim will be the user alice
who has 1000$ in her bank account. Lets simulate the victim authenticating to the bank with the username and password as alice
.
Press enter or click to view image in full size
We have a simple form for transferring funds to other user accounts. Our goal will be to try to maliciously transfer this 1000$ amount to the attacker
user using the CSRF attack.
Now let’s simulate the phishing page which the attacker has sent to Alice at the link http://CatsThatHack.com/index-insecure.html
. Well, Alice really seems to like cat pictures and she just had the sudden urge to click the Transfer Money
button because the cats were urging her to (Of course, real phishing pages would not be like this 🙂).
Press enter or click to view image in full size
And boom, clicking on the button redirected alice
to the bank with her balance of 0$ and saying that the transfer was successful 🫣.
Press enter or click to view image in full size
And just like that — with a single click, Alice’s money disappears. In fact, it doesn’t even have to be a button click , the form can be auto-submitted by the browser without any user interaction at all !!🫠.
Firstly, let’s look at the how the Bank App works.
app.use(
session({
secret: 'csrf-demo-secret',
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true }, // "SameSite" attribute not set explicitly
})
);// Helper: Check if the user is logged in
const requireLogin = (req, res, next) => {
if (!req.session.username || !users[req.session.username]) {
return res.redirect('/login?mustlogin=1');
}
next();
};
// You can only transfer if the user is logged in first
app.post('/transfer', requireLogin, (req, res) => {
const sender = req.session.username;
const { to, amount, coupon } = req.body;
let amt = parseInt(amount, 10);
/*
IRRELEVANT CODE...
*/
users[sender].balance -= amt;
users[to].balance += amt;
res.redirect(redirectUrl);
});
Some important points shown in the above code:
HttpOnly
flag enabled. However, it does not explicitly set the SameSite
attribute, so it defaults to Lax
. You can learn more about what this attribute does in Part 2 of this series.connect.sid
) is issued to the user. The transfer API relies on the presence of a valid session cookie to proceed.Cool, now lets see how the attacker exploited this in his phishing page http://CatsThatHack.com/index-insecure.html
. The attacker’s page code snippet is given below.
<!-- Sends a POST request with top level navigation. Since the cookie is
set as LAX implicitly by the browswer, the POST request is allowed to
be sent for the first 2 minutes -->
<form id="csrfForm" method="POST" action="http://insecure-bank.com/transfer">
<input type="hidden" id="to" name="to" value="attacker">
<input type="hidden" id="amount" name="amount" value="1000">
<input type="hidden" id="coupon" name="coupon" value="">
<button type="submit">Transfer money!</button>
</form>
The hidden form sets the receiver as attacker
and amount as 1000$
and sends a POST request to http://insecure-bank.com/transfer
when the button is clicked. Top Level Navigation and LAX cookies will be discussed in Part 2 of this series so we can ignore that for now 🙂.
Alice’s session cookie is sent with this request by default because the browser automatically attached the session cookie and the web server could not tell that the request was a cross site request. Since the session cookie was sent, this request was assumed to be a legitimate request by our server and the money was sent to the attacker🫠.
We can also verify that the cookie is sent by looking at the developer tools of the browser. Once the Transfer button is clicked on CatsThatHack.com
, we can see the Transfer API request being sent with the session cookie highlighted.
Press enter or click to view image in full size
Welp, now you’ve seen how attackers can exploit CSRF vulnerabilities and how the browser’s default behavior plays a surprisingly big role in making these attacks possible 🫠.
In Part 2 of this series, we’ll dive into how you can actually defend against these type of CSRF attacks. We’ll cover things like SameSite cookies
, different CSRF token strategies
, top-level navigation
, and a lot more!
If you’re building anything that uses sessions, you won’t want to miss it!