Skip to content

CSRF Protection Analyzer ​

Analyzer IDCategorySeverityTime To Fix
csrf-protection🛡️ SecurityCritical20 minutes

What This Checks ​

Detects missing or weak Cross-Site Request Forgery (CSRF) protection in Laravel applications. Validates that forms include @csrf directives, AJAX requests use CSRF tokens, VerifyCsrfToken middleware is properly registered, middleware exception patterns aren't overly broad, and state-changing routes have appropriate protection.

Why It Matters ​

  • Session Hijacking: Attackers can trick authenticated users into performing unwanted actions
  • Financial Loss: CSRF attacks can trigger unauthorized money transfers, purchases, or account changes
  • Data Modification: Unprotected forms allow attackers to modify user data, settings, or passwords
  • Account Takeover: CSRF can change email addresses, passwords, or security settings

Without CSRF protection, an attacker can create a malicious website that submits forms to your application using the victim's authenticated session. When a logged-in user visits the attacker's site, their browser automatically sends their session cookie, allowing the attacker to perform actions as that user without their knowledge or consent.

How to Fix ​

Quick Fix (5 minutes) ​

Scenario 1: Form Missing CSRF Token

blade
<!-- BEFORE - Vulnerable -->
<form method="POST" action="/update-profile">
    <input type="text" name="email">
    <button type="submit">Update</button>
</form>

<!-- AFTER - Protected -->
<form method="POST" action="/update-profile">
    @csrf
    <input type="text" name="email">
    <button type="submit">Update</button>
</form>

Scenario 2: AJAX Request Without Token

blade
<!-- Add meta tag to layout -->
<head>
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>

<!-- Update AJAX call -->
<script>
$.ajax({
    url: '/api/users',
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    },
    data: { name: 'John' }
});
</script>

Scenario 3: Middleware Not Registered

php
// bootstrap/app.php — CSRF is included by default in the 'web' group.
// If you removed it, re-enable it:
return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware): void {
        // The 'web' group includes ValidateCsrfToken by default.
        // Only needed if you previously disabled it:
        $middleware->web(append: [
            \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
        ]);
    })
php
// app/Http/Kernel.php — Add to 'web' middleware group
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,  // ✅ Add this
    ],
];

Proper Fix (20 minutes) ​

Implement comprehensive CSRF protection across your application:

1. Add CSRF Meta Tag to Layout

blade
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>@yield('title')</title>
</head>
<body>
    @yield('content')
</body>
</html>

2. Protect All Forms

blade
<!-- All POST/PUT/PATCH/DELETE forms need @csrf -->
<form method="POST" action="/profile">
    @csrf
    <!-- form fields -->
</form>

<!-- Forms with method spoofing -->
<form method="POST" action="/resource/1">
    @csrf
    @method('PUT')
    <!-- form fields -->
</form>

<!-- Delete forms -->
<form method="POST" action="/resource/1">
    @csrf
    @method('DELETE')
    <button type="submit">Delete</button>
</form>

3. Configure AJAX Globally

javascript
// resources/js/bootstrap.js
import axios from 'axios';

// Configure axios (if using Axios)
let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
    axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found');
}

// Configure jQuery (if using jQuery)
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': token ? token.content : ''
    }
});

4. Fetch API Implementation

javascript
// resources/js/app.js
function getCsrfToken() {
    return document.querySelector('meta[name="csrf-token"]').content;
}

// Use in fetch requests
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': getCsrfToken()
    },
    body: JSON.stringify({ name: 'John' })
});

5. Configure Middleware Exceptions Carefully

Only exclude routes from CSRF protection when absolutely necessary (webhooks, third-party integrations, API routes). The analyzer intelligently evaluates exception patterns.

Laravel 9/10 - Using Middleware $except Array:

php
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
    protected $except = [
        // ✅ ALLOWED - Known webhook/integration services
        // Single-segment patterns are allowed for recognized services:
        'stripe/*',           // Stripe webhooks
        'mailgun/*',          // Mailgun webhooks
        'mailslurp/*',        // MailSlurp webhooks
        'twilio/*',           // Twilio webhooks
        'github/*',           // GitHub webhooks
        'paddle/*',           // Paddle webhooks

        // ✅ ALLOWED - Multi-segment patterns (2+ segments)
        // Patterns with 2+ segments are considered specific enough:
        '/clock/switch/*',          // Clock switching endpoints
        '/admin/webhooks/stripe',   // Admin webhook handling
        'api/external/callback/*',  // External API callbacks

        // ✅ ALLOWED - API routes (typically use token authentication)
        'api/*',

        // ✅ ALLOWED - Specific exact routes
        '/clock/in',
        '/clock/out',
        '/admin/email-templates/upload',

        // ❌ CRITICAL - Never exclude all routes
        // '*',              // Disables ALL CSRF protection
        // '/*',             // Same as above

        // ❌ HIGH SEVERITY - Too broad (single segment, non-recognized service)
        // 'admin/*',        // Entire admin area unprotected
        // 'dashboard/*',    // Entire dashboard unprotected
        // 'webhooks/*',     // Generic webhooks - be more specific
    ];
}

Laravel 11+ - Using validateCsrfTokens() in bootstrap/app.php:

In Laravel 11+, ValidateCsrfToken is included globally by default. Configure exceptions in bootstrap/app.php:

php
// bootstrap/app.php
use Illuminate\Foundation\Application;

->withMiddleware(function (Middleware $middleware) {
    // Exclude specific URIs from CSRF protection
    $middleware->validateCsrfTokens(except: [
        // ✅ ALLOWED - Known webhook/integration services
        'stripe/*',           // Stripe webhooks
        'mailslurp/*',        // MailSlurp webhooks
        'twilio/*',           // Twilio webhooks
        'github/*',           // GitHub webhooks

        // ✅ ALLOWED - Multi-segment patterns (2+ segments)
        '/clock/switch/*',          // Clock switching endpoints
        'api/external/callback/*',  // External API callbacks

        // ✅ ALLOWED - API routes
        'api/*',

        // ✅ ALLOWED - Specific exact routes
        '/clock/in',
        '/clock/out',
        '/admin/email-templates/upload',

        // ❌ CRITICAL - Never exclude all routes
        // '*',              // Disables ALL CSRF protection
        // '/*',             // Same as above

        // ❌ HIGH SEVERITY - Too broad
        // 'admin/*',        // Entire admin area unprotected
        // 'dashboard/*',    // Entire dashboard unprotected
    ]);
})

⚠️ Important for Laravel 11+:

CSRF protection (ValidateCsrfToken) is enabled by default globally. The analyzer detects these common misconfigurations:

  1. Removing from web group: $middleware->web(remove: [ValidateCsrfToken::class]) - Disables CSRF for all web routes
  2. Removing entirely: $middleware->remove(ValidateCsrfToken::class) - Disables CSRF for all routes
  3. Missing from manual stack: When using $middleware->use([...]) to customize the global middleware stack, you must include ValidateCsrfToken::class
  4. Wildcard exceptions: validateCsrfTokens(except: ['*']) - Disables CSRF for all routes

Only use validateCsrfTokens(except: [...]) for legitimate exceptions like webhooks and third-party integrations.

Exception Pattern Rules (Both Versions):

  • Critical Issue: '*' or '/*' - Disables CSRF for all routes
  • High Severity: Single-segment wildcards like 'admin/*', 'dashboard/*' (unless recognized webhook service)
  • Allowed: Multi-segment patterns like '/clock/switch/*', 'api/external/callback/*'
  • Allowed: Known webhook services like 'stripe/*', 'mailslurp/*', 'twilio/*'
  • Best Practice: Use exact routes when possible instead of wildcards

6. Verify Middleware Registration

Laravel 9/10 - app/Http/Kernel.php:

php
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,  // ✓ Must be present
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

Laravel 11+ - bootstrap/app.php:

php
// ✅ CORRECT - CSRF protection is enabled globally by default
->withMiddleware(function (Middleware $middleware) {
    // ValidateCsrfToken is automatically included
    // No action needed unless excluding specific URIs
})

// ❌ WRONG - Removing CSRF from web middleware group
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(remove: [
        \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
    ]);
})

// ❌ WRONG - Manual middleware stack missing ValidateCsrfToken
->withMiddleware(function (Middleware $middleware) {
    $middleware->use([
        \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
        \Illuminate\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
        // Missing ValidateCsrfToken::class - CSRF protection disabled!
    ]);
})

// ✅ CORRECT - Manual middleware stack with ValidateCsrfToken
->withMiddleware(function (Middleware $middleware) {
    $middleware->use([
        \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
        \Illuminate\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Http\Middleware\ValidatePostSize::class,
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,  // ✓ CSRF enabled
    ]);
})

7. Test Protection Works

bash
# Create test route
# routes/web.php
Route::post('/test-csrf', function () {
    return 'CSRF protection working';
});

# Test without token (should fail with 419)
curl -X POST https://yourapp.com/test-csrf

# Test with token (should succeed)
# Get token from browser console:
# document.querySelector('meta[name="csrf-token"]').content
curl -X POST https://yourapp.com/test-csrf \
  -H "X-CSRF-TOKEN: your-token-here"

8. Handle Token Expiration

javascript
// Handle 419 errors gracefully
axios.interceptors.response.use(
    response => response,
    error => {
        if (error.response.status === 419) {
            alert('Your session has expired. Please refresh the page.');
            window.location.reload();
        }
        return Promise.reject(error);
    }
);

References ​