Cross-Site Request Forgery (CSRF): Complete Guide to Understanding and Prevention

Cross-Site Request Forgery (CSRF): Complete Guide to Understanding and Prevention

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:

  1. Victim logs into their bank (bank.com) – receives authentication cookie
  2. Victim browses a malicious website while still logged into the bank
  3. Malicious site triggers a request to bank.com (transfer money, change email, etc.)
  4. Browser automatically includes the bank authentication cookie
  5. 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

  1. Server generates a unique, unpredictable token for each session/request
  2. Token is embedded in forms and validated on submission
  3. 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 (requires Secure flag)

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

  1. Identify state-changing operations (login, transfer, delete, update)
  2. Inspect the request using browser developer tools
  3. Check for CSRF protection:
    • Is there a CSRF token in the form/request?
    • Does the server validate it?
    • Is the token unique and unpredictable?
  4. Create a proof-of-concept attack page
  5. 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:

  1. Primary: CSRF tokens + SameSite cookies
  2. Secondary: Origin/Referer validation
  3. Tertiary: Custom headers for AJAX
  4. Additional: User confirmation for critical actions
  5. 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/

Leave a Reply

Your email address will not be published. Required fields are marked *