Cross-Site Request Forgery
Set Up #
In order follow along with the class lecture and complete this week’s homework, you will need node installed on your system. You can either install it inside your VM and perform all actions in your virtual machine, or you can install directly on your host machine. For this lab, you’ll be able to perform everything within your regular web browser.
Additionally, you will need git installed in order to clone the needed repositories down to your machine.
Clone the CSRF-Bank-Test Repository
git clone https://github.com/jastardev/CISC350-CSRF-Banking-Site.git
Navigate into the repository
cd CISC350-CSRF-Banking-Site
Install the node dependencies
npm install
Seed the database
node init-db
Start the site
npm start
For this demonstration we also need to set our hosts file on our system to give localhost a domain name we use for the cookie.
On mac/linux, you can use the follow command to open the hosts file.
sudo nano /etc/hosts
At the bottom of that file, append the following line
127.0.0.1 bank.test
Save and close the file.
On windows, you need to open Notepad as the administrator
Then from within Notepad, you’ll want to open C:\Windows\System32\drivers\etc\hosts
Inside that file, append the following line:
127.0.0.1 bank.test
Save and close the file.
We can now visit the site in our browser by navigating to http://bank.test:3000/
Exploitation Steps #
Our website is simulating a very simple banking application, with the home page redirecting immediately to login
When we log in with the user alice, we’ll see they have $2,000 and no transaction history.
Inspecting the cookie storage, we’ll see one cookie for the bank.test domain named authToken containing a JWT that expires 24 hours from now and that has the httpOnly flag set. This cookie is moderately protected (still does not follow all best practices) against the XSS attacks we discussed during our last lecture because we wouldn’t be able to access it via javascript.

The Malicious Site #
This page is hosted by the attacker (in our case, it’s hosted by astar.fyi), and a link to it would generally be sent via a phishing link, but it could also be part of an injected iframe or the code could simply be included in a malicious script that the attacker found a way to inject into a victim’s website.
Our malicious page looks extremely similar to the legitimate site, and calls the user to an action, in this case, clicking a button (which under the hood submits a form) to unlock their account. Note that generally, the attacker would try to automate the execution or submission of the form, but for demonstration purposes, I’ve made the action manual.

When we click unlock, we’ll see in the network tab of dev tools that a request is made to http://bank.test:3000/transfer. The response is a redirection to http://bank.test:3000/dashboard, and we end up in the account of the victim.
Note: You may get an alert about navigating to http, which is insecure, that’s to be expected in this example because our local site is not using SSL. In a production application, this warning wouldn’t exist because the victim site would be using SSL.
We’ll also notice in the dashboard that $1000 dollars was successfully transferred from alice to bob.
NOTE: I think this is a bug in my server code and the lifecycle of the cookie, but sometimes the POST request will fail to attach the cookie to the request if there’s a gap between login and the malicious form being executed. For demonstration purposes, I’ve found that logging out and logging back in, then immediately going and submitting the form on the malicious will make it work as it should.
Explanation on What’s Happening #
The malicious form contains two hidden fields, recipient and amount. These are known fields that the attacker found by inspecting the legitimate bank site when crafting their exploit. When the form gets submitted, the browser is reaching out to bank.test and because it’s trying to be efficient, it sees the cookie it has for bank.test and goes “Here ya go!”, including it with the request.
This isn’t inherently a vulnerability in the browser, there are legitimate reason to send cookies from domain to another; federated authentication for example. However, in this example, the banking server has no way to validate that the request came from the legitimate website and that is the vulnerability.
CSRF Prevention Techniques #
Anti-CSRF Tokens #
- Server generates a random token per session (or per form), embeds it in pages (hidden form field or meta tag).
- Server validates token on any state-changing request.
- Attacker cannot read token from another origin, so they cannot include a valid token in the forged request.
- Pitfalls: tokens must be unpredictable, tied to session, checked server-side, and not leaked to third parties (e.g., via Referrer to external sites).
We can see this in action by cloning the fixed repository:
git clone https://github.com/jastardev/CISC350-CSRF-Banking-Site-Fixed.git
Installing the dependencies
cd CISC350-CSRF-Banking-Site-Fixed
npm install
initializing the DB
node init-db
Starting the server
npm start
Now when we visit the login page, in the form, we’ll see a hidden field containing a CSRF token.
If we try to use our malicious page now, it will still submit the POST request, but we get blocked because our malicious page has no way of known the CSRF token it needs to provide with the request.
Same-Site Cookies #
SameSite=Lax blocks cookies on many cross-site requests (but allows top-level GET navigation). Strict is stricter but often breaks some flows.
Use safe HTTP semantics #
- Never accept GET for actions that change server state because GET requests get around the SameSite=Lax flag. If a GET can perform an action, it’s trivially exploitable by an
<img>tag or link.
Origin / Referer Header Validation #
- Server checks the
Origin(preferred) orRefererheader to ensure request came from same origin. Originis reliable for POST/PUT/DELETE from modern browsers;Referercan be stripped by proxies or privacy settings.- Pitfalls: some browsers or privacy tools remove/modify headers; CORS interacts with these checks.
Require re-auth/2FA for Sensitive Actions #
- For high-risk operations require password re-entry, 2FA, or one-time code.
- Even if CSRF triggers, extra confirmation prevents immediate harm.
What does not prevent CSRF #
- CORS (Cross-Origin Resource Sharing) controls reading responses from cross-origin requests, not whether the browser will send credentials. Misconfiguring CORS can make things worse but CORS itself is not a CSRF mitigation.
- Content Security Policy (CSP) helps against some XSS vectors but not CSRF.
- Authentication alone (cookies, tokens) — you need explicit CSRF defenses.