1. php
  2. /security
  3. /csrf-protection

PHP CSRF Protection

Introduction to CSRF Attacks

Cross-Site Request Forgery (CSRF) is a type of attack that tricks users into performing unintended actions on web applications where they're authenticated. Unlike other web attacks that target vulnerabilities in the application code, CSRF exploits the trust that a web application has in the user's browser.

CSRF attacks are particularly insidious because they leverage the user's existing authentication status. When a user is logged into a web application, their browser automatically includes authentication credentials (cookies, HTTP authentication headers) with every request to that domain. Attackers exploit this automatic behavior to make unauthorized requests on behalf of the victim.

How CSRF Attacks Work

The Attack Scenario: Imagine you're logged into your online banking application in one browser tab. In another tab, you visit a malicious website or receive a crafted email with embedded HTML. Without your knowledge, this malicious content causes your browser to make requests to your bank's website, potentially transferring money or changing account settings.

Why It's Dangerous: CSRF attacks can:

  • Transfer funds or make purchases
  • Change account passwords or email addresses
  • Delete user data or accounts
  • Modify application settings
  • Perform any action the authenticated user is authorized to do

Attack Vectors: CSRF attacks can be delivered through:

  • Malicious websites with hidden forms or JavaScript
  • Email with embedded images or links
  • Social media posts with crafted URLs
  • Advertisements on legitimate websites
  • Any content that can cause the browser to make HTTP requests

Understanding the Mechanics

CSRF attacks succeed because web browsers automatically include authentication credentials with requests. When you're logged into a website, your browser stores session cookies and includes them with every request to that domain, regardless of which website initiated the request.

Example Attack Flow:

  1. User logs into bank.com and receives authentication cookie
  2. User visits malicious site evil.com (in same browser session)
  3. evil.com contains hidden form that submits to bank.com/transfer
  4. Browser automatically includes bank.com authentication cookie
  5. Bank processes transfer request as if user initiated it

CSRF vs. Other Attacks

CSRF vs. XSS: While XSS injects malicious scripts into trusted websites, CSRF forces users to execute unwanted actions on trusted websites. XSS requires finding code injection vulnerabilities, while CSRF exploits the browser's automatic credential inclusion.

CSRF vs. Clickjacking: Clickjacking tricks users into clicking on hidden elements, while CSRF doesn't require any user interaction beyond visiting a malicious page.

CSRF vs. Session Hijacking: Session hijacking steals user credentials, while CSRF uses existing credentials without stealing them.

CSRF Prevention Strategies

Token-Based Protection (Synchronizer Token Pattern)

The most effective CSRF protection method involves generating unique, unpredictable tokens for each user session or form submission. These tokens must be included with state-changing requests and validated server-side.

How It Works:

  1. Server generates cryptographically secure random token
  2. Token is embedded in forms (hidden field) or sent to client
  3. Client includes token with all state-changing requests
  4. Server validates token before processing request
  5. Requests without valid tokens are rejected

Why It Works: Attackers cannot predict or obtain the token since it's:

  • Generated server-side with cryptographic randomness
  • Not accessible through cross-origin requests (Same-Origin Policy)
  • Unique per session or form submission

Modern browsers support the SameSite cookie attribute, which provides built-in CSRF protection by controlling when cookies are sent with cross-site requests.

SameSite Values:

  • Strict: Cookies never sent with cross-site requests
  • Lax: Cookies sent with top-level navigation but not embedded requests
  • None: Cookies sent with all cross-site requests (requires Secure flag)

This pattern involves sending the same random value both as a cookie and as a request parameter. The server validates that both values match.

Advantages:

  • Stateless (no server-side token storage required)
  • Effective against CSRF attacks
  • Easier to implement in distributed systems

How It Works:

  1. Set random value in cookie and form field
  2. Attacker cannot read cookie value due to Same-Origin Policy
  3. Server validates cookie value matches form field value

Implementing CSRF Protection in PHP

Basic Token-Based Protection

<?php
class CSRFProtection
{
    private const TOKEN_NAME = 'csrf_token';
    private const TOKEN_LENGTH = 32;
    
    /**
     * Generate a cryptographically secure CSRF token
     * 
     * This method creates a random token that cannot be predicted
     * by attackers. The token is stored in the user's session
     * and must be included with all state-changing requests.
     */
    public static function generateToken(): string
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // Generate cryptographically secure random bytes
        $token = bin2hex(random_bytes(self::TOKEN_LENGTH));
        
        // Store token in session for later validation
        $_SESSION[self::TOKEN_NAME] = $token;
        
        return $token;
    }
    
    /**
     * Validate CSRF token from request
     * 
     * This method checks that the submitted token matches
     * the token stored in the user's session. It uses
     * hash_equals() for timing-attack resistant comparison.
     */
    public static function validateToken(string $submittedToken = null): bool
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // Get token from session
        $sessionToken = $_SESSION[self::TOKEN_NAME] ?? null;
        
        // If no session token exists, validation fails
        if ($sessionToken === null) {
            return false;
        }
        
        // Get submitted token from request if not provided
        if ($submittedToken === null) {
            $submittedToken = $_POST[self::TOKEN_NAME] ?? $_GET[self::TOKEN_NAME] ?? '';
        }
        
        // Use hash_equals for timing-attack resistant comparison
        return hash_equals($sessionToken, $submittedToken);
    }
    
    /**
     * Get HTML hidden input field with CSRF token
     * 
     * This method generates the HTML needed to include
     * the CSRF token in forms. The token is included
     * as a hidden form field.
     */
    public static function getTokenField(): string
    {
        $token = self::generateToken();
        return '<input type="hidden" name="' . self::TOKEN_NAME . '" value="' . htmlspecialchars($token) . '">';
    }
    
    /**
     * Get current CSRF token value
     * 
     * Returns the current token value for use in
     * JavaScript requests or other scenarios where
     * you need the raw token value.
     */
    public static function getToken(): string
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // Return existing token or generate new one
        return $_SESSION[self::TOKEN_NAME] ?? self::generateToken();
    }
    
    /**
     * Middleware function to protect routes from CSRF
     * 
     * This function can be used as middleware to automatically
     * validate CSRF tokens for state-changing requests.
     * It should be called before processing any POST, PUT,
     * PATCH, or DELETE requests.
     */
    public static function protectRoute(): void
    {
        $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
        
        // Only validate tokens for state-changing requests
        if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
            if (!self::validateToken()) {
                // Log potential CSRF attack
                error_log('CSRF token validation failed for ' . $_SERVER['REQUEST_URI'] . ' from IP ' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
                
                // Return 403 Forbidden
                http_response_code(403);
                die('CSRF token validation failed. This request has been blocked for security reasons.');
            }
        }
    }
    
    /**
     * Regenerate CSRF token
     * 
     * This method generates a new token and invalidates
     * the previous one. Useful after sensitive operations
     * or for additional security.
     */
    public static function regenerateToken(): string
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // Always generate new token
        unset($_SESSION[self::TOKEN_NAME]);
        return self::generateToken();
    }
}

// Example usage in a form
session_start();
?>

<!DOCTYPE html>
<html>
<head>
    <title>Protected Form Example</title>
</head>
<body>
    <h2>Transfer Money</h2>
    <form method="POST" action="process_transfer.php">
        <?php echo CSRFProtection::getTokenField(); ?>
        
        <label for="recipient">Recipient Account:</label>
        <input type="text" id="recipient" name="recipient" required>
        
        <label for="amount">Amount:</label>
        <input type="number" id="amount" name="amount" step="0.01" required>
        
        <button type="submit">Transfer Funds</button>
    </form>
</body>
</html>

Advanced CSRF Protection with Per-Form Tokens

<?php
class AdvancedCSRFProtection
{
    private const TOKEN_PREFIX = 'csrf_';
    private const TOKEN_EXPIRY = 3600; // 1 hour
    
    /**
     * Generate form-specific CSRF token
     * 
     * This creates unique tokens for each form, providing
     * additional security by ensuring tokens can only be
     * used for their intended purpose.
     */
    public static function generateFormToken(string $formName, string $action = ''): string
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // Create form-specific token key
        $tokenKey = self::TOKEN_PREFIX . $formName . '_' . md5($action);
        
        // Generate token with expiry
        $tokenData = [
            'token' => bin2hex(random_bytes(32)),
            'expires' => time() + self::TOKEN_EXPIRY,
            'form' => $formName,
            'action' => $action
        ];
        
        $_SESSION[$tokenKey] = $tokenData;
        
        return $tokenData['token'];
    }
    
    /**
     * Validate form-specific CSRF token
     * 
     * This validates that the token is correct, not expired,
     * and was generated for the specific form and action.
     */
    public static function validateFormToken(string $formName, string $submittedToken, string $action = ''): bool
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        $tokenKey = self::TOKEN_PREFIX . $formName . '_' . md5($action);
        $tokenData = $_SESSION[$tokenKey] ?? null;
        
        if (!$tokenData) {
            return false;
        }
        
        // Check if token has expired
        if (time() > $tokenData['expires']) {
            unset($_SESSION[$tokenKey]);
            return false;
        }
        
        // Validate form and action match
        if ($tokenData['form'] !== $formName || $tokenData['action'] !== $action) {
            return false;
        }
        
        // Validate token value
        $isValid = hash_equals($tokenData['token'], $submittedToken);
        
        // One-time use: remove token after validation
        if ($isValid) {
            unset($_SESSION[$tokenKey]);
        }
        
        return $isValid;
    }
    
    /**
     * Clean up expired tokens
     * 
     * This method removes expired tokens from the session
     * to prevent session bloat.
     */
    public static function cleanupExpiredTokens(): void
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        $currentTime = time();
        
        foreach ($_SESSION as $key => $value) {
            if (strpos($key, self::TOKEN_PREFIX) === 0 && is_array($value)) {
                if (isset($value['expires']) && $currentTime > $value['expires']) {
                    unset($_SESSION[$key]);
                }
            }
        }
    }
}

// Usage example for different forms
class SecureFormHandler
{
    public function showTransferForm()
    {
        $token = AdvancedCSRFProtection::generateFormToken('transfer', 'bank_transfer');
        
        echo '<form method="POST" action="process_transfer.php">';
        echo '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
        echo '<input type="hidden" name="form_name" value="transfer">';
        echo '<input type="hidden" name="form_action" value="bank_transfer">';
        // ... other form fields
        echo '</form>';
    }
    
    public function processTransfer()
    {
        $formName = $_POST['form_name'] ?? '';
        $formAction = $_POST['form_action'] ?? '';
        $submittedToken = $_POST['csrf_token'] ?? '';
        
        if (!AdvancedCSRFProtection::validateFormToken($formName, $submittedToken, $formAction)) {
            http_response_code(403);
            die('Invalid CSRF token for transfer form');
        }
        
        // Process the transfer...
        echo "Transfer processed successfully";
    }
}
?>
<?php
class DoubleSubmitCSRF
{
    private const COOKIE_NAME = 'csrf_cookie';
    private const FORM_FIELD = 'csrf_token';
    private const TOKEN_EXPIRY = 86400; // 24 hours
    
    /**
     * Generate double submit CSRF protection
     * 
     * This method implements the double submit cookie pattern
     * by setting the same random value as both a cookie and
     * returning it for inclusion in forms.
     */
    public static function generateDoubleSubmitToken(): string
    {
        // Generate cryptographically secure token
        $token = bin2hex(random_bytes(32));
        
        // Set secure cookie
        setcookie(
            self::COOKIE_NAME,
            $token,
            [
                'expires' => time() + self::TOKEN_EXPIRY,
                'path' => '/',
                'domain' => '', // Use current domain
                'secure' => isset($_SERVER['HTTPS']), // Only send over HTTPS if available
                'httponly' => true, // Not accessible via JavaScript
                'samesite' => 'Strict' // CSRF protection via SameSite
            ]
        );
        
        return $token;
    }
    
    /**
     * Validate double submit CSRF token
     * 
     * This validates that the cookie value matches the
     * submitted form value, providing CSRF protection
     * without server-side state storage.
     */
    public static function validateDoubleSubmitToken(): bool
    {
        $cookieToken = $_COOKIE[self::COOKIE_NAME] ?? '';
        $formToken = $_POST[self::FORM_FIELD] ?? $_GET[self::FORM_FIELD] ?? '';
        
        // Both tokens must exist and match
        return !empty($cookieToken) && !empty($formToken) && hash_equals($cookieToken, $formToken);
    }
    
    /**
     * Get HTML for double submit protection
     * 
     * This generates both the cookie and the hidden form field
     * needed for double submit CSRF protection.
     */
    public static function getDoubleSubmitField(): string
    {
        $token = self::generateDoubleSubmitToken();
        return '<input type="hidden" name="' . self::FORM_FIELD . '" value="' . htmlspecialchars($token) . '">';
    }
    
    /**
     * Middleware for double submit CSRF protection
     * 
     * This can be used to automatically validate double submit
     * tokens for state-changing requests.
     */
    public static function protectWithDoubleSubmit(): void
    {
        $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
        
        if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
            if (!self::validateDoubleSubmitToken()) {
                error_log('Double submit CSRF validation failed for ' . $_SERVER['REQUEST_URI']);
                http_response_code(403);
                die('CSRF protection validation failed');
            }
        }
    }
}
?>

AJAX and Single Page Applications

Modern web applications often use AJAX for dynamic content updates. CSRF protection for AJAX requests requires special consideration:

<?php
// API endpoint that provides CSRF token for AJAX requests
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    // Provide token for AJAX requests
    echo json_encode([
        'csrf_token' => CSRFProtection::getToken()
    ]);
    exit;
}

// Validate CSRF token for API requests
CSRFProtection::protectRoute();

// Process API request...
echo json_encode(['status' => 'success']);
?>
// JavaScript implementation for AJAX CSRF protection
class CSRFHelper {
    static async getToken() {
        try {
            const response = await fetch('/api/csrf-token');
            const data = await response.json();
            return data.csrf_token;
        } catch (error) {
            console.error('Failed to get CSRF token:', error);
            throw error;
        }
    }
    
    static async makeSecureRequest(url, options = {}) {
        const token = await this.getToken();
        
        // Add CSRF token to request
        if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method.toUpperCase())) {
            if (options.body instanceof FormData) {
                options.body.append('csrf_token', token);
            } else {
                const body = options.body ? JSON.parse(options.body) : {};
                body.csrf_token = token;
                options.body = JSON.stringify(body);
                
                options.headers = {
                    'Content-Type': 'application/json',
                    ...options.headers
                };
            }
        }
        
        return fetch(url, options);
    }
}

// Usage example
document.getElementById('transfer-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    
    try {
        const response = await CSRFHelper.makeSecureRequest('/api/transfer', {
            method: 'POST',
            body: formData
        });
        
        if (response.ok) {
            alert('Transfer completed successfully');
        } else {
            alert('Transfer failed');
        }
    } catch (error) {
        console.error('Request failed:', error);
        alert('An error occurred');
    }
});

Best Practices and Security Considerations

Token Generation Best Practices

Use Cryptographically Secure Random Values: Always use random_bytes() or similar cryptographically secure random number generators. Never use rand() or mt_rand() for security tokens.

Sufficient Token Length: Use at least 128 bits (16 bytes) of randomness. The examples above use 32 bytes for extra security margin.

Timing Attack Resistance: Always use hash_equals() for token comparison to prevent timing attacks that could leak token information.

Session Security

Secure Session Configuration: Ensure sessions are configured securely:

  • Use HTTPS-only cookies in production
  • Set appropriate session timeout values
  • Regenerate session IDs after authentication

Session Fixation Protection: Regenerate session IDs after login and other privilege changes to prevent session fixation attacks.

Additional Security Layers

SameSite Cookies: Use SameSite=Strict or SameSite=Lax for session cookies to provide built-in CSRF protection in modern browsers.

Referrer Validation: Check the HTTP Referer header as an additional validation layer, but don't rely on it alone as it can be spoofed or omitted.

Content Security Policy: Implement CSP headers to prevent inline scripts that could be used in CSRF attacks.

Testing CSRF Protection

<?php
class CSRFTest
{
    public static function testCSRFProtection()
    {
        echo "<h3>Testing CSRF Protection</h3>";
        
        // Test 1: Valid token should pass
        session_start();
        $token = CSRFProtection::generateToken();
        $_POST['csrf_token'] = $token;
        
        if (CSRFProtection::validateToken()) {
            echo "✅ Valid token test passed<br>";
        } else {
            echo "❌ Valid token test failed<br>";
        }
        
        // Test 2: Invalid token should fail
        $_POST['csrf_token'] = 'invalid_token';
        
        if (!CSRFProtection::validateToken()) {
            echo "✅ Invalid token test passed<br>";
        } else {
            echo "❌ Invalid token test failed<br>";
        }
        
        // Test 3: Missing token should fail
        unset($_POST['csrf_token']);
        
        if (!CSRFProtection::validateToken()) {
            echo "✅ Missing token test passed<br>";
        } else {
            echo "❌ Missing token test failed<br>";
        }
        
        // Test 4: Token reuse should fail (if using one-time tokens)
        $token = CSRFProtection::generateToken();
        $_POST['csrf_token'] = $token;
        
        CSRFProtection::validateToken(); // First use
        
        if (!CSRFProtection::validateToken()) {
            echo "✅ Token reuse prevention test passed<br>";
        } else {
            echo "⚠️ Token reuse prevention test failed (may be expected for session-based tokens)<br>";
        }
    }
}

// Run tests in development environment
if ($_ENV['APP_ENV'] === 'development') {
    CSRFTest::testCSRFProtection();
}
?>

For comprehensive web application security:

Summary

CSRF protection is essential for any web application that performs state-changing operations. Key principles include:

Token-Based Protection: Generate unpredictable tokens and validate them with every state-changing request.

Defense in Depth: Combine multiple protection methods including CSRF tokens, SameSite cookies, and referrer validation.

Proper Implementation: Use cryptographically secure random number generation and timing-attack resistant comparisons.

User Experience: Implement CSRF protection transparently without degrading the user experience.

Modern Standards: Leverage modern browser features like SameSite cookies for enhanced protection.

CSRF attacks exploit the trust between web applications and user browsers. By implementing proper CSRF protection, you ensure that all state-changing requests are intentionally initiated by authenticated users, not by malicious third parties. Remember that CSRF protection should be part of a comprehensive security strategy that includes other measures like input validation, XSS protection, and secure authentication systems.