skip to content

In Laravel 13, how does PreventRequestForgery use the Sec-Fetch-Site header, and what changes when you enable originOnly or allowSameSite?

level: seniorimportance: should knowfreq 30%

answer

  1. same-origin passes before any token
  2. sent by browsers over HTTPS only
  3. originOnly: 403 OriginMismatchException
  4. no XSRF-TOKEN cookie in origin-only mode
  5. allowSameSite admits sibling subdomains

basics

~10 s

PreventRequestForgery accepts a write when Sec-Fetch-Site is same-origin, otherwise falls back to the CSRF token. originOnly drops the fallback, answering 403 via OriginMismatchException; allowSameSite also accepts same-site values from sibling subdomains.

solid answer

~40 s

In Laravel 13, after skipping reads, test runs and excluded URIs, the middleware reads `Sec-Fetch-Site`. `same-origin` passes immediately; `same-site` passes only when `allowSameSite` is on. Otherwise it falls back to comparing the `_token`/`X-CSRF-TOKEN`/`X-XSRF-TOKEN` token and throws `TokenMismatchException` (419) on failure. `$middleware->preventRequestForgery(originOnly: true)` removes the fallback: any write whose header is not accepted throws `OriginMismatchException`, rendered as 403, and the `XSRF-TOKEN` cookie is no longer set. Browsers send the header only over HTTPS, so origin-only mode rejects every write over plain HTTP and from clients that omit it. `allowSameSite: true` lets `dashboard.example.com` accept writes from `example.com`, but it also trusts every other subdomain of the same site. Set all options in one call, because each call resets the booleans.

code

php · 12 lines
php
<?php

use Illuminate\Foundation\Configuration\Middleware;

// bootstrap/app.php, inside Application::configure(...)
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(
        except: ['webhooks/payments/*'],
        originOnly: true,     // no token fallback; failures are 403
        allowSameSite: false, // only same-origin writes pass
    );
})

go deeper

for a junior

Know that Laravel 13's CSRF middleware trusts same-origin browser requests before it looks at the token.

for a middle

Explain the middleware's decision order and the difference between a 419 token mismatch and a 403 origin mismatch.

for a senior

Decide when originOnly or allowSameSite is safe for a deployment, covering HTTPS everywhere, client mix, subdomain ownership and monitoring.

for a principal

Weigh dropping token plumbing against browser dependence and subdomain trust, and set the default posture for the organisation's apps.

## The header being checked `Sec-Fetch-Site` is a **fetch metadata** request header that modern browsers attach to requests sent over secure (HTTPS) connections. It states the relationship between the page that initiated the request and the target: - `same-origin`: same scheme, host and port; - `same-site`: a different origin under the same registrable site, such as `example.com` and `dashboard.example.com`; - `cross-site`: a different site altogether; - `none`: a user-initiated navigation such as typing the URL. Pages cannot set or override it from script, which is what makes it usable as a forgery signal. ## The decision order in PreventRequestForgery For every request in the `web` group, Laravel 13's middleware returns early, letting the request through, on the first condition that holds: 1. `GET`, `HEAD` or `OPTIONS` method; 2. the app is running unit tests; 3. the URI is in the `except` list; 4. `Sec-Fetch-Site` is `same-origin`, or `same-site` with `allowSameSite` enabled; 5. the request's token matches the session token. If nothing matches, it throws `TokenMismatchException` ("CSRF token mismatch."), which the handler renders as **419**. ## What the two options change Both are named arguments of `$middleware->preventRequestForgery()` in `bootstrap/app.php`. | Setting | Step 4 | Step 5 (token fallback) | Failure | `XSRF-TOKEN` cookie | |---|---|---|---|---| | default | `same-origin` only | yes | 419 | set | | `allowSameSite: true` | `same-origin` or `same-site` | yes | 419 | set | | `originOnly: true` | `same-origin` only | **no** | `OriginMismatchException`, **403** | not set | | both | `same-origin` or `same-site` | no | 403 | not set | In origin-only mode the check throws as soon as the header is not acceptable, so no token is ever read, and the middleware stops adding the `XSRF-TOKEN` cookie because nothing would consume it. ## When origin-only mode fits Origin-only mode removes the token plumbing (`@csrf`, meta tags, the Axios cookie dance) and relies on the browser. It fits only when: - the app is always served over **HTTPS**, since browsers omit the header on plain HTTP and every write would then be refused with 403; - every client that writes is a **modern browser** that sends fetch metadata; - machine clients such as webhooks are already excluded or routed outside the `web` group. Local development over plain HTTP is the usual casualty: forms that worked yesterday start returning 403. ## What allowSameSite trusts `allowSameSite: true` exists for setups like a marketing site on `example.com` posting to an app on `app.example.com`. The cost is breadth: it accepts writes initiated from **any** origin on the same site, including a user-content subdomain or a forgotten staging host. Enable it only when you control every subdomain of the site. ## Operating notes - **One call.** `preventRequestForgery()` applies `except` additively but sets `originOnly` and `allowSameSite` on every call, defaulting to `false`. Two calls with different arguments leave only the last booleans in force. - **Status codes differ.** Monitoring that counts 419s as "session expired" will not see origin-only rejections, which arrive as 403. - **Tests skip it.** The middleware returns early while unit tests run, so neither mode is exercised by ordinary feature tests. - **Upgrading.** Code that referenced `VerifyCsrfToken` or `validateCsrfTokens()` keeps working through deprecated aliases, but new configuration should use `PreventRequestForgery` and `preventRequestForgery()`.

  • Why do forms start failing with 403 in local development after enabling originOnly?
    Browsers send `Sec-Fetch-Site` only over secure connections. A local site served over plain HTTP gets no header, and in origin-only mode the middleware has no token fallback, so it throws `OriginMismatchException`, rendered as 403. Serve local development over HTTPS or keep the token fallback.
  • Why does a 403 from origin-only mode not appear in dashboards that track CSRF failures by status 419?
    The two failures use different exceptions: `TokenMismatchException` maps to 419, while `OriginMismatchException` maps to 403. Both are on the handler's internal do-not-report list, so neither is logged by default. Alerting keyed only on 419 misses origin-only rejections entirely.

saying these in an interview costs you the question

  • originOnly still falls back to the token when the header is missing.
  • allowSameSite only admits requests from the exact same host.
  • Sec-Fetch-Site can be set by page JavaScript, so it proves nothing.
  • Origin-only failures return 419 just like token mismatches.
  • The Sec-Fetch-Site check works the same over plain HTTP.