You upgraded to Laravel 13, ran php artisan route:list, and noticed VerifyCsrfToken is gone. In its place: PreventRequestForgery. The release notes call it a high-impact security change. Most apps work without touching a line of code, but a couple of edge cases will catch you if you don't know what changed under the hood.
Token-based CSRF and its weak spot#
Laravel's classic CSRF protection has been the same shape since the early versions. Every form gets a hidden _token field, every state-changing request gets compared against the session token, and a mismatch raises a 419. It works — until the token leaks. An XSS bug, an open redirect, a chatty server-side log, a developer leaving a tab open in DevTools: any of these can hand a valid token to an attacker, and the token check then waves the malicious request straight through.
The defence has always been "don't leak the token." That's a hard guarantee to make across a real-world codebase. Origin verification answers a different question entirely: regardless of whether a token is valid, did this request originate from a page on your own origin? If the browser says no, the request is rejected before the token check ever runs. Two layers, two independent failure modes — a classic defence-in-depth pattern.
What PreventRequestForgery adds#
The renamed middleware does both checks in sequence. It first inspects the Sec-Fetch-Site header that modern browsers attach to every outgoing request. The header tells your application whether the request originated from same-origin, same-site, cross-site, or was initiated by the user typing into the address bar (none). When the value is same-origin, the middleware passes the request through without looking at the token at all.
When the header is missing — older browser, plain HTTP, or non-browser client — or the value indicates a cross-origin request, the middleware falls back to the classic token check. From your application code's perspective, nothing else changes: the @csrf directive still emits a hidden token field, your AJAX setups still send X-CSRF-TOKEN, and your tests still get the middleware skipped automatically when you call php artisan test.
How the middleware is registered#
In Laravel 11 onwards, middleware configuration lives in bootstrap/app.php. PreventRequestForgery is attached to the web group by default, so a fresh Laravel 13 install already has it wired up:
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// PreventRequestForgery is already attached to the web group.
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Upgrading from Laravel 12? VerifyCsrfToken and ValidateCsrfToken remain as deprecated aliases pointing at PreventRequestForgery, so existing references still compile. Update direct references anywhere you have them — most commonly in withoutMiddleware() calls inside feature tests:
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
// Laravel <= 12.x
->withoutMiddleware([VerifyCsrfToken::class]);
// Laravel 13+
->withoutMiddleware([PreventRequestForgery::class]);
The full set of Laravel 13 breaking changes is worth a separate read — I covered the upgrade in detail in the Laravel 12 to 13 upgrade guide.
Opt into origin-only mode#
By default the middleware runs both checks. If your application is served exclusively over HTTPS to modern browsers, you can drop the token fallback and rely on origin verification alone. Pass originOnly: true to preventRequestForgery in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->preventRequestForgery(originOnly: true);
})
Two behavioural changes follow:
- Failed checks return a
403 Forbiddeninstead of the familiar419 Page Expired. - Requests over plain HTTP — including any local development environment without TLS — will fail outright, because browsers omit
Sec-Fetch-Siteon insecure connections.
I run staging and production behind HTTPS exclusively with originOnly mode enabled, and Herd locally with herd secure so the dev experience matches. If you serve anything over plain HTTP, or expect older clients like embedded WebViews or kiosk browsers, leave the default two-layer mode in place.
Cross-subdomain requests#
A common production layout: dashboard.example.com posting to api.example.com. Browsers treat that as same-site but cross-origin, so the default same-origin-only check fails. Allow same-site requests alongside same-origin:
->withMiddleware(function (Middleware $middleware) {
$middleware->preventRequestForgery(allowSameSite: true);
})
That's the cleanest way to handle subdomain APIs without disabling the middleware. The browser still attaches Sec-Fetch-Site: same-site, the check still runs, and you avoid maintaining an allow-list of origins.
For SPAs talking from a different site entirely — app.example.com calling api.othercompany.com — the right answer is Laravel Sanctum, which sets up stateful authentication and a CSRF-aware cookie flow specifically for this. Don't try to allow-list third-party origins through the middleware.
Exclude routes from CSRF protection#
Stripe webhooks and other server-to-server callbacks don't ship with Sec-Fetch-Site or a CSRF token — they're not browser requests. The cleanest pattern is keeping those routes outside the web middleware group entirely (in routes/api.php or a dedicated routes/webhooks.php). If you must keep them in web, exclude them at the middleware config level:
->withMiddleware(function (Middleware $middleware) {
$middleware->preventRequestForgery(except: [
'stripe/*',
'webhooks/github',
]);
})
Disabling CSRF doesn't mean disabling authentication. Webhook endpoints still need their own integrity check — see verifying Stripe webhook signatures in Laravel for the right shape of that, and apply fine-grained rate limiting so a leaked webhook URL can't be hammered.
Gotchas and Edge Cases#
A few traps I've hit on real upgrades:
Local dev over HTTP fails origin-only mode. If you've flipped originOnly: true, you have to serve dev over HTTPS too. herd secure (or valet secure) is the one-line fix. Without it, every form in dev returns 403 and you'll waste an hour blaming the middleware.
Reverse proxies that strip Sec-Fetch-Site. Nginx and most load balancers forward all browser headers by default, so this is usually fine. The trap is bespoke header allow-lists in nginx (proxy_set_header) that don't include Sec-Fetch-Site. If you see same-origin requests falling through to the token check, dump the request headers in middleware and verify the header is arriving.
Tests skip the middleware entirely. Laravel auto-disables PreventRequestForgery during php artisan test, so your existing suite won't catch misconfigurations. To test the middleware behaviour explicitly, call $this->withMiddleware(PreventRequestForgery::class) in a Pest feature test and assert the response code for a request missing Sec-Fetch-Site.
Livewire and Inertia just work. Both libraries make same-origin AJAX/fetch requests, so the browser sets Sec-Fetch-Site: same-origin and the middleware passes through without ever consulting the token. No code changes needed on either side.
X-CSRF-TOKEN and X-XSRF-TOKEN still work. The token fallback honours both headers exactly as VerifyCsrfToken did. Axios, Angular, and any other library that auto-populates these headers continues to work unchanged.
Wrapping Up#
For most applications the upgrade to PreventRequestForgery is invisible. Update any VerifyCsrfToken::class references in tests and route definitions, ensure staging and production are served over HTTPS, and decide whether origin-only mode is worth the strictness for your environment. Combine that with native passkeys via Fortify and you've cleared two of Laravel 13's biggest security wins in an afternoon.
FAQ#
What is PreventRequestForgery in Laravel 13?
PreventRequestForgery is the renamed CSRF middleware that ships in Laravel 13's web middleware group. It adds a layer of origin verification using the browser's Sec-Fetch-Site header on top of the classic CSRF token check. When the browser reports a same-origin request, the middleware passes it through; otherwise it falls back to validating the session token. The class lives at Illuminate\Foundation\Http\Middleware\PreventRequestForgery.
Is VerifyCsrfToken removed in Laravel 13?
No. VerifyCsrfToken and ValidateCsrfToken are kept as deprecated aliases that point at PreventRequestForgery, so existing code that references those class names still compiles. Direct references should be updated to PreventRequestForgery::class for forward compatibility, particularly in withoutMiddleware() calls inside tests and route definitions, but no immediate breakage occurs if you leave them in place.
How do I configure trusted origins for PreventRequestForgery?
There's no explicit "trusted origins" allow-list in PreventRequestForgery because the middleware uses the browser-set Sec-Fetch-Site header instead of header allow-lists. To accept cross-subdomain requests (for example a dashboard on a sibling subdomain), call $middleware->preventRequestForgery(allowSameSite: true) in bootstrap/app.php. To exclude specific URIs, pass them as except: ['stripe/*', ...]. For cross-site SPA traffic, use Laravel Sanctum's stateful cookie flow rather than trying to allow-list origins at the middleware layer.
Does PreventRequestForgery break my existing Blade forms?
No. Same-origin form submissions — the standard case for a Blade form posting back to its own application — get a Sec-Fetch-Site: same-origin header from the browser, so the middleware passes them through without checking the token. If a request arrives without Sec-Fetch-Site (older browser, HTTP connection) the middleware falls back to the classic token validation, which your existing @csrf directives already satisfy. Either way, no template changes are required.
How does origin-aware CSRF protection differ from token-based CSRF?
Token-based CSRF verifies that the request carries a session-bound secret that an attacker shouldn't be able to read. Origin-aware CSRF verifies that the request actually came from a page on your own origin, regardless of whether a token is present. The token model fails if the token leaks; the origin model fails only if the browser misreports Sec-Fetch-Site, which modern browsers won't do over HTTPS. PreventRequestForgery runs both checks so a failure in either layer still rejects the request.
Do I need to update my SPA to work with PreventRequestForgery?
If your SPA is served from the same origin as your Laravel backend, no — browsers set Sec-Fetch-Site: same-origin automatically and the middleware passes the request through. If the SPA is on a different subdomain, opt into allowSameSite: true in bootstrap/app.php. If it's on a different site entirely, route the authentication through Laravel Sanctum's stateful cookie flow, which handles both the origin and token concerns properly for cross-site SPAs.