CSRF Protection

Introduction

Cross-site request forgeries are a type of malicious exploit whereby unauthorized commands are performed on behalf of an authenticated user. Slenix makes it easy to protect your application from cross-site request forgery (CSRF) attacks.

An Explanation of the Vulnerability

To understand how this vulnerability can be exploited, consider an example. Imagine your application has a /user/email route that accepts a POST request to change the authenticated user's email address. Without CSRF protection, a malicious website could create an HTML form that points to your application's route and submits the attacker's own email address:

html
<form action="https://your-application.com/user/email" method="POST">
    <input type="email" value="malicious@example.com">
</form>

<script>
    document.forms[0].submit();
</script>

If the malicious website automatically submits the form when the page is loaded, the attacker only needs to lure an unsuspecting user of your application to visit their website — and that user's email address will be silently changed.

To prevent this vulnerability, Slenix inspects every incoming POST, PUT, PATCH, or DELETE request for a secret session value that a malicious application is unable to access.


Preventing CSRF Requests

Slenix automatically generates a CSRF token for each active user session. This token is used to verify that the authenticated user is the one actually making requests to the application. Since the token is stored in the user's session and changes each time the session is regenerated, a malicious application cannot access it.

The current session's CSRF token can be accessed via the csrf_token() helper function:

php
use Slenix\Http\Routing\Router;

Router::get('/token', function ($request, $response) {
    $token = csrf_token();

    // use $token as needed
});

Any time you define a POST, PUT, PATCH, or DELETE HTML form in your application, you should include a hidden CSRF _csrf_token field so that the CSRF protection can validate the request. For convenience, you may use the @csrf Luna directive to generate the hidden token input field:

html
<form method="POST" action="/profile">
    @csrf

    <!-- Equivalent to... -->
    <input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
</form>

Slenix automatically validates the CSRF token on every POST, PUT, PATCH, and DELETE request dispatched through the router. When the token in the request matches the token stored in the session, Slenix knows that the authenticated user is the one initiating the request. If the tokens do not match, a 419 status code is returned.

Regenerating the Token

You may regenerate the CSRF token at any time — for example, after a user logs in or logs out — using the CSRF::regenerate() method:

php
use Slenix\Supports\Security\CSRF;

CSRF::regenerate();

Note: Regenerating the token invalidates the previous token immediately. Any open forms rendered before the regeneration will fail validation on submission.


Excluding URIs From CSRF Protection

Sometimes you may wish to exclude a set of URIs from CSRF protection. For example, if you are using Stripe to process payments and utilizing their webhook system, you will need to exclude your Stripe webhook handler from CSRF protection since Stripe will not know what CSRF token to send.

You may exclude specific URIs by calling CSRF::except() in your application's bootstrap file, passing an array of URI patterns. Wildcards (*) are supported:

php
use Slenix\Supports\Security\CSRF;

CSRF::except([
    '/api/*',
    '/webhook/stripe',
    '/webhook/*',
]);

Alternatively, you may place webhook or API routes outside the standard web routing by using a dedicated routes file that is bootstrapped without CSRF validation.


X-CSRF-TOKEN

In addition to checking for the CSRF token as a POST parameter, Slenix also checks for the X-CSRF-TOKEN request header on every state-mutating request. This makes it straightforward to protect AJAX-based applications.

Store the token in an HTML <meta> tag using the CSRF::meta() helper or the csrf_token() function:

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

You can then read the token from JavaScript and attach it to all outgoing requests. With the native fetch API:

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

fetch('/profile', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': token,
    },
    body: JSON.stringify({ name: 'Cláudio' }),
});

With Axios, you can configure the header globally so every request includes it automatically:

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

axios.defaults.headers.common['X-CSRF-TOKEN'] = token;

With jQuery's $.ajaxSetup:

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

X-XSRF-TOKEN

Slenix also accepts the X-XSRF-TOKEN request header, which is the convention used by some JavaScript frameworks and HTTP clients such as Angular and Axios on same-origin requests.

To use this header, store the token in a JavaScript-accessible cookie or pass it via a <meta> tag and read it in your client-side code:

js
// Axios reads XSRF-TOKEN cookie automatically on same-origin requests.
// For manual usage:
const token = getCookie('XSRF-TOKEN');

fetch('/settings', {
    method: 'PUT',
    headers: {
        'X-XSRF-TOKEN': token,
    },
});

CSRF in Luna Templates

Luna provides the @csrf directive as a shorthand for generating the hidden token field. Use it inside every form that submits to a POST, PUT, PATCH, or DELETE route:

html
<form method="POST" action="{{ route('profile.update') }}">
    @csrf
    @method('PUT')

    <input type="text" name="name" value="@old('name')">
    <button type="submit">Save</button>
</form>

The @csrf directive compiles to:

html
<input type="hidden" name="_csrf_token" value="a1b2c3...">

CSRF and the Router

Slenix validates the CSRF token automatically inside Router::dispatch() before invoking the route handler. No middleware configuration is required for standard web routes.

The validation runs for all state-mutating HTTP methods:

MethodCSRF Validated
GETNo
HEADNo
OPTIONSNo
POSTYes
PUTYes
PATCHYes
DELETEYes

If validation fails, the router returns a 419 response:

php
// From Router::dispatch() — runs automatically before every handler
if (self::shouldValidateCsrf($method)) {
    if (!self::validateCsrfToken()) {
        $response->status(419);
        throw new \RuntimeException('419 — CSRF token invalid or expired.');
    }
}

Skipping CSRF Validation for API Routes

If you are building an API that authenticates via JWT or another token-based mechanism, you may want to exclude your API routes entirely. Place them in a route group with a prefix of api/ and register the pattern in CSRF::except():

php
use Slenix\Supports\Security\CSRF;
use Slenix\Http\Routing\Router;

// Bootstrap — runs before Router::dispatch()
CSRF::except(['/api/*']);

// routes/web.php
Router::group(['prefix' => 'api/v1', 'middleware' => ['jwt']], function () {
    Router::get('/users',      [Api\UserController::class, 'index']);
    Router::post('/users',     [Api\UserController::class, 'store']);
    Router::put('/users/{id}', [Api\UserController::class, 'update']);
});

CSRF Helper Reference

The Slenix\Supports\Security\CSRF class exposes the following static methods:

MethodReturnsDescription
CSRF::token()stringReturns the current session CSRF token, generating one if absent
CSRF::regenerate()stringGenerates and stores a new token, returning it
CSRF::verify()boolReturns true if the request token matches the session token
CSRF::verifyOrFail()voidThrows a RuntimeException with status 419 if verification fails
CSRF::field()stringReturns a hidden <input> element containing the token
CSRF::meta()stringReturns a <meta name="csrf-token"> tag for use in JavaScript
CSRF::fieldAndMeta()stringReturns both the meta tag and the hidden field
CSRF::isSafeMethod()boolReturns true for GET, HEAD, OPTIONS
CSRF::shouldVerify()boolReturns true for all non-safe methods
CSRF::except(array)voidRegisters URI patterns to exclude from CSRF validation
CSRF::isExcluded()boolReturns true if the current URI matches an exclusion pattern

Global Helper Functions

Slenix also provides global helper functions for use in templates and controllers:

php
// Returns the raw token string
$token = csrf_token();

// Returns the full <input type="hidden"> field
$field = csrf_field();

In Luna templates, prefer the directives:

html
@csrf        {{-- hidden input field --}}
{{ csrf_token() }}   {{-- raw token string --}}