CSRF Protection

Cross-Site Request Forgery (CSRF) protection prevents malicious sites from performing unauthorized actions on behalf of your users.

How CSRF Attacks Work

CSRF attacks exploit the trust a site has in a user's browser. If a user is logged into your site, an attacker's site could submit a form to your site, and the browser would include the user's session cookie.

Protection Mechanism

ZephyrPHP generates a unique token for each user session. This token must be included in all POST, PUT, PATCH, and DELETE requests. Since attackers can't know this token, forged requests will be rejected.

Using CSRF Protection

HTML Forms

Include the CSRF field in every form:

<form method="POST" action="/posts">
    {{ csrf_field()|raw }}

    <input type="text" name="title">
    <textarea name="content"></textarea>
    <button type="submit">Create Post</button>
</form>

The csrf_field() function generates:

<input type="hidden" name="_token" value="abc123def456...">

Just the Token

If you need just the token value:

<meta name="csrf-token" content="{{ csrf_token() }}">

AJAX Requests

For JavaScript requests, include the token in a header:

Setup

<!-- In your HTML head -->
<meta name="csrf-token" content="{{ csrf_token() }}">

Fetch API

const token = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': token
    },
    body: JSON.stringify({
        title: 'My Post',
        content: 'Post content...'
    })
});

Axios

// Set default header for all requests
axios.defaults.headers.common['X-CSRF-TOKEN'] =
    document.querySelector('meta[name="csrf-token"]').content;

// Now all requests include the token
axios.post('/api/posts', { title: 'My Post' });

jQuery

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

$.post('/api/posts', { title: 'My Post' });

Session Integration

The CSRF token is managed through the session:

<?php

// Get current CSRF token
$token = session()->csrf();

// Generate a new token
$newToken = session()->regenerateCsrf();

// Verify a token manually
if (session()->verifyCsrf($submittedToken)) {
    // Token is valid
}

Verifying Tokens

ZephyrPHP automatically verifies CSRF tokens for:

  • POST requests
  • PUT requests
  • PATCH requests
  • DELETE requests

The token is checked from:

  1. _token in the request body
  2. X-CSRF-TOKEN header

Manual Verification

Verify tokens manually when needed:

<?php

use ZephyrPHP\Security\Security;

class PaymentController extends Controller
{
    public function process()
    {
        $security = new Security();
        $token = $this->request->input('_token')
            ?? $this->request->header('X-CSRF-TOKEN');

        if (!$security->verifyCsrf($token)) {
            abort(403, 'CSRF token mismatch');
        }

        // Process payment...
    }
}

Excluding Routes

Some routes may need to accept external webhooks without CSRF verification. Configure exceptions in your middleware:

<?php

class VerifyCsrfToken
{
    protected array $except = [
        '/webhooks/stripe',
        '/webhooks/github',
        '/api/external/*'
    ];

    public function handle($request, $next)
    {
        if ($this->shouldVerify($request)) {
            $this->verifyCsrfToken($request);
        }

        return $next($request);
    }

    protected function shouldVerify($request): bool
    {
        foreach ($this->except as $pattern) {
            if ($request->pathMatches($pattern)) {
                return false;
            }
        }
        return true;
    }
}

Token Regeneration

Regenerate the CSRF token after sensitive actions:

<?php

class AuthController extends Controller
{
    public function login()
    {
        // Authenticate user...

        // Regenerate token after login
        $this->session->regenerateCsrf();

        return $this->redirect('/dashboard');
    }

    public function logout()
    {
        // Clear session and regenerate token
        $this->session->destroy();

        return $this->redirect('/');
    }
}

Form Method Spoofing

HTML forms only support GET and POST. For PUT, PATCH, and DELETE, use method spoofing with CSRF:

<form method="POST" action="/posts/1">
    {{ csrf_field()|raw }}
    <input type="hidden" name="_method" value="DELETE">
    <button type="submit">Delete Post</button>
</form>

<form method="POST" action="/posts/1">
    {{ csrf_field()|raw }}
    <input type="hidden" name="_method" value="PUT">
    <input type="text" name="title" value="Updated Title">
    <button type="submit">Update Post</button>
</form>

Troubleshooting

Token Mismatch Errors

If you're getting CSRF token mismatch errors:

  1. Check the token is included - Ensure csrf_field() is in your form
  2. Check session is active - The session must be started before generating/verifying tokens
  3. Check token hasn't expired - Sessions expire; ensure users resubmit forms after long inactivity
  4. Check AJAX headers - Ensure the X-CSRF-TOKEN header is being sent
Best Practice

Always include CSRF protection in forms, even for seemingly harmless actions like search. It's better to be consistently secure than to try to determine which actions "need" protection.