Security & Performance

CSRF Protection

18 min Lesson 4 of 35

Understanding CSRF Attacks

Cross-Site Request Forgery (CSRF) is a web security vulnerability that allows attackers to trick users into performing unintended actions on a web application where they're authenticated. CSRF attacks exploit the trust that a web application has in the user's browser, forcing authenticated users to execute unwanted actions without their knowledge or consent.

Unlike XSS attacks that exploit the user's trust in a particular site, CSRF exploits the site's trust in the user's browser. When a user is authenticated to a website, their browser automatically includes authentication credentials (cookies, session tokens) with every request to that site. An attacker can craft malicious requests that appear to come from the legitimate user, causing the application to perform actions on behalf of that user.

Key Concept: CSRF attacks work because browsers automatically include credentials (cookies, HTTP authentication) with requests to a domain, regardless of where the request originated. This automatic credential inclusion is the fundamental security issue that CSRF protection mechanisms address.

How CSRF Attacks Work

A typical CSRF attack follows this sequence: First, the victim logs into a legitimate website (bank.com) and receives an authentication cookie. While still logged in, the victim visits a malicious website (evil.com) without logging out. The malicious site contains code that makes a request to bank.com, such as transferring money. Because the victim is still authenticated, their browser automatically includes the authentication cookie with this request. The bank's server receives what appears to be a legitimate request from an authenticated user and processes it, transferring money without the user's knowledge.

<!-- Example: Malicious HTML on evil.com -->\n<img src="https://bank.com/transfer?amount=1000&to=attacker" width="0" height="0">\n\n<!-- Or using a form that auto-submits -->\n<form action="https://bank.com/transfer" method="POST" id="csrf-form">\n <input type="hidden" name="amount" value="1000">\n <input type="hidden" name="to" value="attacker">\n</form>\n<script>document.getElementById('csrf-form').submit();</script>

CSRF attacks can be executed through various vectors including images with malicious src attributes, automatically submitting forms, XMLHttpRequest or Fetch API calls, and even through email HTML content. The attack doesn't require JavaScript and can work with simple HTML elements, making it particularly dangerous.

Important: GET requests should never perform state-changing operations (create, update, delete). Always use POST, PUT, PATCH, or DELETE for operations that modify data. Using GET for state changes makes CSRF attacks trivial to execute through simple image tags or links.

Token-Based CSRF Prevention

The most common and effective CSRF prevention technique is the Synchronizer Token Pattern. This approach generates a unique, unpredictable token for each user session (or even each request) and requires that token to be included with every state-changing request. The server validates that the token in the request matches the token stored in the user's session before processing the request.

The token must be included in a way that browsers don't automatically send it with requests. This is typically done by embedding the token in the HTML form as a hidden field or including it in custom HTTP headers for AJAX requests. Since the malicious site cannot read the token due to Same-Origin Policy, it cannot include it in forged requests.

<!-- Server generates token and includes it in forms -->\n<form action="/transfer" method="POST">\n <input type="hidden" name="csrf_token" value="random_unpredictable_token_here">\n <input type="text" name="amount">\n <input type="text" name="to">\n <button type="submit">Transfer</button>\n</form>\n\n<!-- Server-side validation (PHP example) -->\nif (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {\n die('CSRF token validation failed');\n}\n\n// Process the request...
Best Practice: Use constant-time comparison functions (like PHP's hash_equals()) when validating CSRF tokens to prevent timing attacks. Regular string comparison can leak information about the token through timing differences.

SameSite Cookie Attribute

The SameSite cookie attribute is a modern browser security feature that provides built-in CSRF protection by controlling when browsers send cookies with cross-site requests. This attribute has three possible values: Strict, Lax, and None, each providing different levels of protection and functionality.

SameSite=Strict provides the strongest protection by preventing the browser from sending the cookie with any cross-site request. This means if a user clicks a link to your site from another site, they won't be authenticated on the first page load. SameSite=Lax (the default in modern browsers) allows cookies to be sent with top-level navigations using safe HTTP methods (GET) but blocks them for cross-site POST requests and iframe embeds. SameSite=None explicitly allows cross-site cookie sending but requires the Secure attribute.

// PHP: Setting SameSite attribute\nsetcookie('session_id', $value, [\n 'expires' => time() + 3600,\n 'path' => '/',\n 'domain' => '.example.com',\n 'secure' => true,\n 'httponly' => true,\n 'samesite' => 'Strict' // or 'Lax' or 'None'\n]);\n\n// JavaScript: Checking SameSite support\nif (document.cookie.includes('SameSite')) {\n console.log('Browser supports SameSite cookies');\n}
Browser Support: SameSite cookies are supported in all modern browsers (Chrome 80+, Firefox 69+, Safari 12.1+, Edge 80+). However, some older browsers and certain mobile browsers may not support this feature, so it should be used as defense-in-depth alongside token-based protection.

Double Submit Cookie Pattern

The Double Submit Cookie pattern is an alternative CSRF protection technique that doesn't require server-side session storage. In this approach, a random token is set as both a cookie and a request parameter. The server validates that both values match, ensuring the request came from a legitimate source that has access to the cookie.

This technique works because while an attacker's site can cause the browser to send cookies, it cannot read those cookies due to Same-Origin Policy, nor can it set cookies for your domain. The attacker therefore cannot determine the token value to include in the request parameter.

// Client-side: Set token as cookie and include in requests\nfunction generateCSRFToken() {\n const token = Array.from(crypto.getRandomValues(new Uint8Array(32)))\n .map(b => b.toString(16).padStart(2, '0'))\n .join('');\n \n document.cookie = `csrf_token=${token}; path=/; secure; samesite=strict`;\n return token;\n}\n\n// Include token in AJAX requests\nfetch('/api/transfer', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-CSRF-Token': getCookieValue('csrf_token')\n },\n body: JSON.stringify({ amount: 100, to: 'recipient' })\n});\n\n// Server-side: Compare cookie token with header token\n$cookieToken = $_COOKIE['csrf_token'] ?? '';\n$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';\n\nif (!hash_equals($cookieToken, $headerToken)) {\n http_response_code(403);\n die('CSRF validation failed');\n}
Security Consideration: Double Submit Cookie is vulnerable if an attacker can set cookies on your domain (through subdomain vulnerabilities or cookie injection). For maximum security, combine this with proper cookie scoping and consider token-based protection for critical operations.

CSRF Protection in SPAs and APIs

Single Page Applications (SPAs) and REST APIs require special consideration for CSRF protection. Traditional form-based token embedding doesn't work well with JavaScript-heavy applications that make frequent AJAX requests. Modern approaches typically use custom HTTP headers to transmit CSRF tokens, as these headers cannot be set by simple HTML forms or cross-origin requests.

When building SPAs, the CSRF token is typically provided to the application during initial page load (embedded in HTML or via a dedicated API endpoint) and then stored in JavaScript memory or sessionStorage. For each subsequent API request, the token is included in a custom header like X-CSRF-Token. The server validates this header on every state-changing request.

// Initial token retrieval (embedded in HTML)\n<script>\n window.csrfToken = '<?php echo $_SESSION["csrf_token"]; ?>';\n</script>\n\n// Or via API endpoint\nfetch('/api/csrf-token')\n .then(response => response.json())\n .then(data => {\n localStorage.setItem('csrfToken', data.token);\n });\n\n// Include in all state-changing requests\nconst apiRequest = async (url, method, data) => {\n const headers = {\n 'Content-Type': 'application/json',\n 'X-CSRF-Token': window.csrfToken || localStorage.getItem('csrfToken')\n };\n \n const response = await fetch(url, {\n method,\n headers,\n credentials: 'same-origin', // Important for cookies\n body: JSON.stringify(data)\n });\n \n return response.json();\n};\n\n// Usage\napiRequest('/api/posts', 'POST', { title: 'New Post', content: '...' });
API Design: For purely API-based applications without sessions, consider using token-based authentication (JWT) instead of cookies. This eliminates CSRF vulnerability entirely since tokens must be explicitly included in requests and aren't automatically sent by browsers.

CSRF Protection in Laravel

Laravel provides comprehensive built-in CSRF protection through the VerifyCsrfToken middleware. This middleware automatically generates CSRF tokens for each user session and validates tokens on all POST, PUT, PATCH, and DELETE requests. Laravel makes it easy to include tokens in forms and AJAX requests with helper functions and JavaScript integration.

The @csrf Blade directive automatically generates a hidden input field with the CSRF token. For AJAX requests, Laravel automatically includes the token from the csrf-token meta tag in Axios request headers. You can also manually access the token using the csrf_token() helper function or the CSRF_TOKEN() facade.

<!-- Blade template with CSRF token -->\n<form method="POST" action="/profile">\n @csrf\n <input type="text" name="name">\n <button type="submit">Update Profile</button>\n</form>\n\n<!-- Meta tag for JavaScript access -->\n<meta name="csrf-token" content="{{ csrf_token() }}">\n\n<!-- Axios automatically includes token from meta tag -->\n<script>\naxios.post('/api/profile', {\n name: 'John Doe'\n}).then(response => {\n console.log('Profile updated');\n});\n</script>\n\n// Excluding routes from CSRF protection (use cautiously)\nprotected $except = [\n 'webhook/*', // External webhooks don't have tokens\n 'api/*' // If using API token authentication\n];
Practice Exercise: Create a simple web application with a money transfer form. Implement CSRF protection using both token-based validation and SameSite cookies. Then, create a separate malicious page that attempts to perform CSRF attacks. Test that the protection mechanisms successfully block the attacks while allowing legitimate requests. Try different attack vectors including form auto-submission, image tags, and AJAX requests.

Defense in Depth Strategies

Effective CSRF protection relies on multiple layers of defense working together. While CSRF tokens are the primary defense, combining them with other security measures provides robust protection even if one layer fails. A comprehensive defense strategy includes proper HTTP method usage, SameSite cookies, custom request headers, user interaction requirements, and additional authentication for sensitive operations.

Always use appropriate HTTP methods: GET for read operations, POST/PUT/PATCH for modifications, and DELETE for removals. Implement SameSite cookie attributes to prevent cross-site cookie transmission. Require custom headers for AJAX requests, as these cannot be set by simple HTML forms. For critical operations like password changes or large financial transfers, require additional authentication such as password confirmation or MFA codes.

// Comprehensive CSRF protection implementation\nclass CSRFProtection {\n // Generate cryptographically secure token\n public static function generateToken() {\n if (empty($_SESSION['csrf_token'])) {\n $_SESSION['csrf_token'] = bin2hex(random_bytes(32));\n }\n return $_SESSION['csrf_token'];\n }\n \n // Validate token from request\n public static function validateToken($token) {\n if (empty($_SESSION['csrf_token']) || empty($token)) {\n return false;\n }\n \n // Use constant-time comparison\n return hash_equals($_SESSION['csrf_token'], $token);\n }\n \n // Validate request method\n public static function validateMethod($expected) {\n return $_SERVER['REQUEST_METHOD'] === strtoupper($expected);\n }\n \n // Check for AJAX request\n public static function isAjaxRequest() {\n return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&\n strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';\n }\n \n // Full validation for sensitive operations\n public static function validateRequest() {\n // Check HTTP method\n if (!in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])) {\n return true; // Allow GET requests\n }\n \n // Check CSRF token\n $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';\n if (!self::validateToken($token)) {\n http_response_code(403);\n die('CSRF validation failed');\n }\n \n return true;\n }\n}
Remember: CSRF protection is just one component of web application security. It must be combined with proper authentication, authorization, input validation, output encoding, and other security measures to create a truly secure application. Defense in depth means that if one security control fails, others are in place to prevent exploitation.

Testing CSRF Protection

Thoroughly testing CSRF protection is essential to ensure it works correctly and doesn't interfere with legitimate functionality. Testing should cover both positive cases (legitimate requests succeed) and negative cases (forged requests are blocked). Automated testing can help catch regressions when code changes.

// PHPUnit test for CSRF protection\npublic function test_csrf_token_required_for_post_requests()\n{\n // Attempt POST without CSRF token - should fail\n $response = $this->post('/transfer', [\n 'amount' => 100,\n 'to' => 'recipient'\n ]);\n \n $response->assertStatus(419); // CSRF token mismatch\n}\n\npublic function test_valid_csrf_token_allows_request()\n{\n // Get CSRF token\n $token = csrf_token();\n \n // POST with valid token - should succeed\n $response = $this->post('/transfer', [\n '_token' => $token,\n 'amount' => 100,\n 'to' => 'recipient'\n ]);\n \n $response->assertStatus(200);\n}\n\npublic function test_csrf_token_rotates_after_use()\n{\n $token = csrf_token();\n \n // Use token\n $this->post('/transfer', ['_token' => $token]);\n \n // Verify token changed\n $newToken = csrf_token();\n $this->assertNotEquals($token, $newToken);\n}

Understanding and implementing proper CSRF protection is critical for web application security. By combining token-based validation, SameSite cookies, proper HTTP method usage, and additional authentication for sensitive operations, you can effectively protect your users from CSRF attacks while maintaining a smooth user experience.