Skip to main content

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

csrf-vulnerable-site-login.png

When we log in with the user alice, we’ll see they have $2,000 and no transaction history.

csrf-vulnerable-site-logged-in.png

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.

csrf-auth-cookie.png

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.

Link to Malicious Site

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.

csrf-malicious-page.png

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.

csrf-transfer-success.png

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.

csrf-dashboard-post-transfer.png

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.

csrf-malicious-form-code.png

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.

csrf-anti-csrf-token-in-form.png
This token is generated by the server and injected into the page prior to the page being sent the user’s browser. When the user submits the form, the CSRF token is validated by the server and the server knowns the request came from the proper web page.

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.

csrf-failed-attacked.png

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.

csrf-samesite-lax.png

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) or Referer header to ensure request came from same origin.
  • Origin is reliable for POST/PUT/DELETE from modern browsers; Referer can 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.