Cross-Site Request Forgery (CSRF), also known as “one-click attack” or “session riding,” is a web security vulnerability that tricks authenticated users into executing unwanted actions on a web application. Despite being less publicized than SQL injection or XSS, CSRF attacks can have devastating consequences, from unauthorized fund transfers to account takeovers.
This comprehensive guide explains what CSRF is, how attackers exploit it, and most importantly how to protect your web applications against it.
What is CSRF?
CSRF is an attack that forces an authenticated user to execute unwanted actions on a web application in which they’re currently logged in. The attack works by exploiting the trust that a web application has in the user’s browser.
The Core Problem
When you log into a website, your browser stores authentication credentials (typically as cookies). Every subsequent request to that site automatically includes these credentials. CSRF exploits this automatic credential inclusion.
Key insight: The browser sends cookies automatically with every request to a domain, regardless of where the request originates.
How CSRF Attacks Work
The Attack Scenario
Imagine this sequence:
- Victim logs into their bank (bank.com) – receives authentication cookie
- Victim browses a malicious website while still logged into the bank
- Malicious site triggers a request to bank.com (transfer money, change email, etc.)
- Browser automatically includes the bank authentication cookie
- Bank processes the request as legitimate because it has valid credentials
The victim never intended to perform this action, but the bank can’t distinguish between legitimate and forged requests.
Visual Example
[Victim's Browser]
├─ Tab 1: bank.com (logged in, cookie stored)
└─ Tab 2: evil.com (attacker's site)
│
└─> Sends hidden request to bank.com
↓
[Browser automatically attaches bank.com cookies]
↓
[Bank processes request as legitimate]
Real-World CSRF Attack Examples
Example 1: Unauthorized Money Transfer
Vulnerable bank transfer endpoint:
https://bank.com/transfer?to=attacker&amount=10000
Attacker creates malicious page:
<!-- Hidden in evil.com -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" width="0" height="0">
When a logged-in bank user visits this page, the browser automatically sends the request with their authentication cookies, transferring money to the attacker.
Example 2: Email Change Attack
Vulnerable account endpoint:
POST https://website.com/account/change-email
email=at******@**il.com
Attacker’s malicious form:
<form action="https://website.com/account/change-email" method="POST" id="csrf">
<input type="hidden" name="email" value="at******@**il.com">
</form>
<script>
document.getElementById('csrf').submit();
</script>
The victim’s email is changed without their knowledge, allowing the attacker to initiate password reset.
Example 3: Social Media Post
Vulnerable posting endpoint:
POST https://social.com/post
content=Click%20this%20link%20to%20win%20prizes!%20http://evil.com
The attacker tricks logged-in users into posting spam to their social media accounts.
Types of CSRF Attacks
1. GET-Based CSRF
Exploits GET requests that modify data (a poor practice, but still common):
<!-- Simple image tag attack -->
<img src="https://vulnerable-site.com/delete-account?confirm=yes">
<!-- Link attack -->
<a href="https://vulnerable-site.com/transfer?to=attacker&amount=5000">
Click here for free gift!
</a>
2. POST-Based CSRF
More complex, but exploits POST requests:
<form action="https://vulnerable-site.com/change-password" method="POST">
<input type="hidden" name="new_password" value="hacked123">
</form>
<script>
document.forms[0].submit();
</script>
3. JSON-Based CSRF
Targets modern APIs that accept JSON:
fetch('https://vulnerable-api.com/account/update', {
method: 'POST',
credentials: 'include', // Sends cookies
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'at******@**il.com'})
});
Real-World CSRF Vulnerabilities
Notable Incidents
- 2008 – YouTube CSRF: Attackers could perform actions on behalf of users
- 2010 – Netflix: CSRF vulnerability allowed unauthorized account changes
- 2014 – eBay: CSRF flaw enabled attackers to hijack user sessions
- 2018 – Coinbase: CSRF issue in account settings
These vulnerabilities affected millions of users and led to security overhauls.
Prevention: Comprehensive Defense Strategies
1. CSRF Tokens (Synchronizer Token Pattern)
This is the most common and effective defense mechanism.
How It Works
- Server generates a unique, unpredictable token for each session/request
- Token is embedded in forms and validated on submission
- Attacker cannot guess or obtain the token, so their forged requests fail
Implementation Examples
PHP Implementation:
// Generate token (on page load or session start)
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Include token in form
?>
<form method="POST" action="transfer.php">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<input type="text" name="amount" placeholder="Amount">
<input type="text" name="recipient" placeholder="Recipient">
<button type="submit">Transfer</button>
</form>
<?php
// Validate token on form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token validation failed');
}
// Process the legitimate request
$amount = $_POST['amount'];
$recipient = $_POST['recipient'];
// ... perform transfer
}
Python/Flask Implementation:
from flask import Flask, session, request, render_template
from flask_wtf.csrf import CSRFProtect
import secrets
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
csrf = CSRFProtect(app)
@app.route('/transfer', methods=['GET', 'POST'])
def transfer():
if request.method == 'POST':
# CSRF token automatically validated by Flask-WTF
amount = request.form['amount']
recipient = request.form['recipient']
# Process transfer
return "Transfer successful"
return render_template('transfer.html')
Template with CSRF token:
<form method="POST">
{{ csrf_token() }}
<input type="text" name="amount">
<input type="text" name="recipient">
<button type="submit">Transfer</button>
</form>
Node.js/Express Implementation:
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
app.use(csrfProtection);
// Render form with CSRF token
app.get('/transfer', (req, res) => {
res.render('transfer', { csrfToken: req.csrfToken() });
});
// Validate CSRF token (automatic)
app.post('/transfer', (req, res) => {
// Token validated by middleware
const { amount, recipient } = req.body;
// Process transfer
res.send('Transfer successful');
});
// Error handling for invalid tokens
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).send('CSRF token validation failed');
} else {
next(err);
}
});
React/SPA Implementation:
// Fetch CSRF token on app load
async function getCsrfToken() {
const response = await fetch('/api/csrf-token', {
credentials: 'include'
});
const data = await response.json();
return data.csrfToken;
}
// Include token in requests
async function transferMoney(amount, recipient) {
const csrfToken = await getCsrfToken();
const response = await fetch('/api/transfer', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ amount, recipient })
});
return response.json();
}
2. SameSite Cookie Attribute
Modern browsers support the SameSite attribute for cookies, which restricts when cookies are sent.
// PHP - Set SameSite attribute
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'yourdomain.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict' // or 'Lax'
]);
session_start();
// Express.js
app.use(session({
secret: 'your-secret',
cookie: {
sameSite: 'strict', // or 'lax'
secure: true,
httpOnly: true
}
}));
SameSite Values:
Strict: Cookie only sent for same-site requests (most secure, may break some legitimate flows)Lax: Cookie sent for same-site requests and top-level navigation (balanced)None: Cookie sent with all requests (requiresSecureflag)
Browser Support: Well-supported in modern browsers (Chrome 51+, Firefox 60+, Safari 12+)
3. Double Submit Cookie Pattern
Alternative to server-side token storage:
// Set CSRF token in cookie and request header
function setCSRFToken() {
const token = generateRandomToken();
document.cookie = `csrf_token=${token}; Secure; SameSite=Strict`;
return token;
}
// Include token in requests
fetch('/api/transfer', {
method: 'POST',
credentials: 'include',
headers: {
'X-CSRF-Token': getTokenFromCookie('csrf_token')
},
body: JSON.stringify({ amount: 100, recipient: 'user123' })
});
Server validation:
// Compare cookie value with header value
$cookieToken = $_COOKIE['csrf_token'] ?? '';
$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!hash_equals($cookieToken, $headerToken)) {
http_response_code(403);
die('CSRF validation failed');
}
4. Custom Request Headers
APIs can require custom headers that cannot be set cross-origin:
// Client includes custom header
fetch('/api/transfer', {
method: 'POST',
credentials: 'include',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: 100 })
});
// Server validates header presence
if (!isset($_SERVER['HTTP_X_REQUESTED_WITH']) ||
$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
http_response_code(403);
die('Invalid request');
}
Limitation: This doesn’t work for simple requests and can be bypassed in some scenarios.
5. Origin and Referer Header Validation
Check that requests come from your own domain:
// Validate Origin header
$allowedOrigins = ['https://yourdomain.com', 'https://www.yourdomain.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (!in_array($origin, $allowedOrigins)) {
http_response_code(403);
die('Invalid origin');
}
// Validate Referer header (fallback)
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$refererHost = parse_url($referer, PHP_URL_HOST);
if ($refererHost !== 'yourdomain.com' && $refererHost !== 'www.yourdomain.com') {
http_response_code(403);
die('Invalid referer');
}
Limitations:
- Headers can be missing (user privacy settings)
- Some proxies strip these headers
- Should be used as additional defense, not primary
6. User Interaction for Sensitive Actions
Require re-authentication or CAPTCHA for critical operations:
// Require password confirmation for sensitive actions
if ($_POST['action'] === 'delete_account') {
if (!password_verify($_POST['current_password'], $user['password_hash'])) {
die('Password confirmation required');
}
// Proceed with deletion
}
// Require CAPTCHA for high-value transactions
if ($_POST['amount'] > 10000) {
if (!verifyCaptcha($_POST['captcha_response'])) {
die('CAPTCHA verification required');
}
}
Framework-Specific CSRF Protection
Laravel (PHP)
// CSRF protection enabled by default
// Blade template
<form method="POST" action="/transfer">
@csrf
<input type="text" name="amount">
<button type="submit">Transfer</button>
</form>
// AJAX requests
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
</script>
Django (Python)
# settings.py - CSRF middleware enabled by default
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
]
# Template
<form method="POST">
{% csrf_token %}
<input type="text" name="amount">
<button type="submit">Transfer</button>
</form>
# AJAX requests
// Get token from cookie
function getCookie(name) {
let cookieValue = null;
if (document.cookie) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
fetch('/transfer/', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
Ruby on Rails
# Enabled by default in ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
# View template
<%= form_with url: transfer_path do |form| %>
<%= form.text_field :amount %>
<%= form.submit "Transfer" %>
<% end %>
# AJAX
// Rails includes CSRF token in meta tag
$.ajax({
url: '/transfer',
type: 'POST',
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token',
$('meta[name="csrf-token"]').attr('content'));
}
});
ASP.NET
// Razor view
@using (Html.BeginForm("Transfer", "Account"))
{
@Html.AntiForgeryToken()
<input type="text" name="amount" />
<button type="submit">Transfer</button>
}
// Controller validation
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Transfer(TransferModel model)
{
// Process transfer
return View();
}
Best Practices and Additional Considerations
1. Use POST for State-Changing Operations
// WRONG - GET request changes state
<a href="/delete-account?confirm=yes">Delete Account</a>
// CORRECT - POST request with CSRF protection
<form method="POST" action="/delete-account">
<input type="hidden" name="csrf_token" value="...">
<button type="submit">Delete Account</button>
</form>
2. Implement Proper Session Management
// Regenerate session ID on login
session_regenerate_id(true);
// Set secure session configuration
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
3. Secure CORS Configuration
// Be restrictive with CORS
header('Access-Control-Allow-Origin: https://trusted-domain.com');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-CSRF-Token');
4. Token Management Best Practices
- Generate cryptographically strong tokens: Use
random_bytes()or equivalent - Unique per session: Each session should have its own token
- Consider per-request tokens: For highly sensitive applications
- Token rotation: Regenerate tokens periodically
- Secure storage: Store tokens securely on the server side
// Cryptographically secure token generation
function generateCSRFToken() {
return bin2hex(random_bytes(32)); // 64 characters
}
// Constant-time comparison to prevent timing attacks
function validateCSRFToken($userToken, $sessionToken) {
return hash_equals($sessionToken, $userToken);
}
Testing for CSRF Vulnerabilities
Manual Testing Steps
- Identify state-changing operations (login, transfer, delete, update)
- Inspect the request using browser developer tools
- Check for CSRF protection:
- Is there a CSRF token in the form/request?
- Does the server validate it?
- Is the token unique and unpredictable?
- Create a proof-of-concept attack page
- Test with a logged-in session in a different tab
Testing Checklist
✅ All state-changing operations use POST (not GET)
✅ CSRF tokens present in all forms
✅ Tokens validated on the server side
✅ Tokens are unique and unpredictable
✅ SameSite cookie attribute is set
✅ Sensitive actions require re-authentication
✅ Proper CORS configuration
Automated Testing Tools
- Burp Suite: CSRF token detection and testing
- OWASP ZAP: Automated CSRF vulnerability scanning
- CSRFTester: Specialized CSRF testing tool
- Nikto: Web server scanner with CSRF checks
Common Mistakes and Pitfalls
❌ Mistake 1: Using GET for State Changes
// VULNERABLE
if ($_GET['action'] === 'delete' && $_GET['id']) {
deleteUser($_GET['id']);
}
❌ Mistake 2: Checking Referer Only
// INSUFFICIENT
if (strpos($_SERVER['HTTP_REFERER'], 'trusted-site.com') !== false) {
// Process request
}
// Referer can be missing or stripped
❌ Mistake 3: Weak Token Generation
// VULNERABLE
$csrf_token = md5(session_id()); // Predictable!
❌ Mistake 4: Not Validating Tokens
// VULNERABLE - Token present but not validated
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
// No server-side validation!
❌ Mistake 5: Including Token in GET Requests
// VULNERABLE - Token exposed in URL/logs
<a href="/transfer?csrf_token=abc123&amount=1000">Transfer</a>
Defense in Depth Strategy
Implement multiple layers of protection:
- Primary: CSRF tokens + SameSite cookies
- Secondary: Origin/Referer validation
- Tertiary: Custom headers for AJAX
- Additional: User confirmation for critical actions
- Monitoring: Log and alert on suspicious patterns
Compliance and Standards
CSRF protection is required by:
- OWASP Top 10 – A01:2021 Broken Access Control
- PCI DSS – Requirement 6.5.9
- NIST – Secure coding guidelines
- CWE-352 – Cross-Site Request Forgery
Conclusion
CSRF attacks exploit the trust that web applications place in authenticated users’ browsers. While the attack vector is straightforward, the consequences can be severe from unauthorized transactions to complete account takeover.
Key Takeaways:
✅ Implement CSRF tokens for all state-changing operations
✅ Use SameSite cookies as an additional layer of defense
✅ Never use GET for operations that modify data
✅ Validate tokens server-side with constant-time comparison
✅ Require re-authentication for sensitive actions
✅ Test thoroughly during development and in production
✅ Leverage framework protections when available
By implementing these defenses, you can effectively protect your web applications and users from CSRF attacks.
Additional Resources
- OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- PortSwigger CSRF Guide: https://portswigger.net/web-security/csrf
- CWE-352: Cross-Site Request Forgery
- MDN Web Docs: SameSite cookies documentation
Stay secure and keep learning. For more cybersecurity articles and tutorials, visit blog.cybersamir.com/