CSRF - Cross Site Request Forgery
Cross-Site Request Forgery (CSRF) is a web security vulnerability that allows an attacker to induce users to perform actions that they do not intend to perform. It occurs when a malicious website, email, or program causes a user's web browser to perform an unwanted action on a trusted site where the user is currently authenticated.
CSRF attacks specifically target state-changing requests, not theft of data, since the attacker cannot see the response to the forged request. With a little help from social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker's choosing.
How CSRF Attacks Work
The fundamental principle behind CSRF is that web browsers automatically include credentials (cookies, authentication headers, etc.) with requests to a domain. When a user is authenticated to a website, their browser stores session cookies that are sent with every subsequent request to that domain.
Here's the typical CSRF attack flow:
- User Authentication: The victim logs into a legitimate website (e.g., their banking site)
- Session Establishment: The website sets a session cookie in the user's browser
- Malicious Request: While still logged in, the user visits a malicious website or clicks a malicious link
- Forged Request: The malicious site triggers a request to the legitimate website
- Automatic Authentication: The browser automatically includes the session cookie with the request
- Unauthorized Action: The legitimate website processes the request as if it came from the authenticated user
PHP Web Application Example
Let's examine a vulnerable PHP application and then see how to protect it.
Vulnerable Application
Consider a simple banking application with the following structure:
index.php (Login page)
<?php
session_start();
if ($_POST['username'] && $_POST['password']) {
// Simple authentication (in real apps, use proper password hashing)
if ($_POST['username'] === 'user' && $_POST['password'] === 'password') {
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $_POST['username'];
$_SESSION['balance'] = 1000; // Initial balance
header('Location: dashboard.php');
exit;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Bank Login</title>
</head>
<body>
<h2>Bank Login</h2>
<form method="POST">
<input type="text" name="username" placeholder="Username" required><br><br>
<input type="password" name="password" placeholder="Password" required><br><br>
<button type="submit">Login</button>
</form>
</body>
</html>
dashboard.php (User dashboard)
<?php
session_start();
if (!$_SESSION['authenticated']) {
header('Location: index.php');
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Bank Dashboard</title>
</head>
<body>
<h2>Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!</h2>
<p>Your balance: $<?php echo $_SESSION['balance']; ?></p>
<h3>Transfer Money</h3>
<form action="transfer.php" method="POST">
<input type="text" name="recipient" placeholder="Recipient" required><br><br>
<input type="number" name="amount" placeholder="Amount" required><br><br>
<button type="submit">Transfer</button>
</form>
<br><a href="logout.php">Logout</a>
</body>
</html>
transfer.php (Vulnerable transfer handler)
<?php
session_start();
if (!$_SESSION['authenticated']) {
header('Location: index.php');
exit;
}
if ($_POST['recipient'] && $_POST['amount']) {
$recipient = $_POST['recipient'];
$amount = (int)$_POST['amount'];
if ($amount > 0 && $amount <= $_SESSION['balance']) {
$_SESSION['balance'] -= $amount;
// In a real application, you would transfer to the recipient
echo "<h2>Transfer Successful!</h2>";
echo "<p>Transferred $" . $amount . " to " . htmlspecialchars($recipient) . "</p>";
echo "<p>Remaining balance: $" . $_SESSION['balance'] . "</p>";
echo '<a href="dashboard.php">Back to Dashboard</a>';
} else {
echo "<h2>Transfer Failed!</h2>";
echo "<p>Insufficient funds or invalid amount.</p>";
echo '<a href="dashboard.php">Back to Dashboard</a>';
}
} else {
header('Location: dashboard.php');
}
?>
The CSRF Attack
Now, let's create a malicious website that exploits this vulnerability:
malicious-site.html
<!DOCTYPE html>
<html>
<head>
<title>Innocent Looking Website</title>
</head>
<body>
<!-- Hidden malicious form -->
<form id="maliciousForm" action="http://vulnerable-bank.com/transfer.php" method="POST" style="display: none;">
<input type="hidden" name="recipient" value="[email protected]">
<input type="hidden" name="amount" value="500">
</form>
<script>
document.getElementById('maliciousForm').submit();
</script>
</body>
</html>
When a logged-in user visits this malicious site and clicks the "More Cats!" button, their browser will submit a POST request to the banking site, transferring $500 to the attacker's account. Since the user is still logged in, their session cookie will be automatically included, making the request appear legitimate.
CSRF Protection Methods
1. CSRF Tokens (Synchronizer Token Pattern)
The most common and effective defense against CSRF is using CSRF tokens. Here's how to implement it:
Updated dashboard.php with CSRF token
<?php
session_start();
if (!$_SESSION['authenticated']) {
header('Location: index.php');
exit;
}
// Generate CSRF token
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Bank Dashboard</title>
</head>
<body>
<h2>Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!</h2>
<p>Your balance: $<?php echo $_SESSION['balance']; ?></p>
<h3>Transfer Money</h3>
<form action="transfer.php" method="POST">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<input type="text" name="recipient" placeholder="Recipient" required><br><br>
<input type="number" name="amount" placeholder="Amount" required><br><br>
<button type="submit">Transfer</button>
</form>
<br><a href="logout.php">Logout</a>
</body>
</html>
Updated transfer.php with CSRF protection
<?php
session_start();
if (!$_SESSION['authenticated']) {
header('Location: index.php');
exit;
}
// CSRF Token validation
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('<h2>CSRF Attack Detected!</h2><p>Invalid or missing CSRF token.</p><a href="dashboard.php">Back to Dashboard</a>');
}
if ($_POST['recipient'] && $_POST['amount']) {
$recipient = $_POST['recipient'];
$amount = (int)$_POST['amount'];
if ($amount > 0 && $amount <= $_SESSION['balance']) {
$_SESSION['balance'] -= $amount;
// Regenerate CSRF token after successful action
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
echo "<h2>Transfer Successful!</h2>";
echo "<p>Transferred $" . $amount . " to " . htmlspecialchars($recipient) . "</p>";
echo "<p>Remaining balance: $" . $_SESSION['balance'] . "</p>";
echo '<a href="dashboard.php">Back to Dashboard</a>';
} else {
echo "<h2>Transfer Failed!</h2>";
echo "<p>Insufficient funds or invalid amount.</p>";
echo '<a href="dashboard.php">Back to Dashboard</a>';
}
} else {
header('Location: dashboard.php');
}
?>
2. SameSite Cookie Attribute
Modern browsers support the SameSite cookie attribute, which helps prevent CSRF attacks:
// Set session cookie with SameSite attribute
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'your-domain.com',
'secure' => true, // Only send over HTTPS
'httponly' => true, // Prevent XSS access
'samesite' => 'Strict' // or 'Lax'
]);
session_start();
3. Double Submit Cookie Pattern
Another approach is the double submit cookie pattern:
// Set CSRF cookie
if (!isset($_COOKIE['csrf_token'])) {
$csrf_token = bin2hex(random_bytes(32));
setcookie('csrf_token', $csrf_token, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => false, // JavaScript needs access
'samesite' => 'Strict'
]);
} else {
$csrf_token = $_COOKIE['csrf_token'];
}
// In forms, include the token as a hidden field
// In AJAX requests, read from cookie and include in headers
4. Custom Request Headers
For AJAX requests, require custom headers:
// JavaScript AJAX request with custom header
fetch('/transfer.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Protection': '1'
},
body: JSON.stringify({
recipient: '[email protected]',
amount: 100
})
});
// PHP validation
if (!isset($_SERVER['HTTP_X_CSRF_PROTECTION'])) {
die('CSRF protection header missing');
}
Best Practices for CSRF Prevention
1. Always Validate CSRF Tokens
- Generate cryptographically strong tokens
- Validate tokens on every state-changing request
- Regenerate tokens after successful actions
2. Use Proper HTTP Methods
- Use POST, PUT, DELETE for state-changing operations
- Never use GET requests for actions that modify data
3. Implement Proper Session Management
// Secure session configuration
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);
4. Content-Type Validation
// Only accept JSON for API endpoints
if ($_SERVER['CONTENT_TYPE'] !== 'application/json') {
http_response_code(400);
die('Invalid content type');
}
5. Origin and Referer Checking
// Check Origin header
$allowed_origins = ['https://your-domain.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (!in_array($origin, $allowed_origins)) {
http_response_code(403);
die('Forbidden origin');
}
Testing for CSRF Vulnerabilities
Manual Testing
- Log into the application
- Capture a state-changing request
- Create a malicious HTML page that replicates the request
- Visit the malicious page while logged in
- Check if the action was executed
Automated Testing Tools
- OWASP ZAP
- Burp Suite
- CSRFTester
Real-World Impact
CSRF vulnerabilities can lead to:
- Unauthorized financial transactions
- Account takeovers
- Data modification or deletion
- Privilege escalation
- Social engineering attacks
Notable real-world examples include attacks on major platforms like Gmail, Netflix, and various banking applications.