1. What is CSRF?
Cross-Site Request Forgery (CSRF) is an attack that tricks a logged-in user’s browser into sending unwanted requests to your application.
If you don’t defend against it, an attacker can:
-
change a user’s email
-
transfer money
-
delete records
-
…anything the victim is authorised to do
The trick? The malicious request comes from the victim’s own browser, so it passes session authentication unless you have an extra “secret” check.
2. How Laravel solves the problem
Laravel’s built-in middleware VerifyCsrfToken
adds a unique, random token to every active session:
-
Token generated and stored in the session.
-
The token is embedded into every HTML <form> you render with the
@csrf
Blade directive (or the helpercsrf_field()
). -
On POST, PUT, PATCH, or DELETE requests, the middleware compares the submitted token to the one saved in the session.
-
If they match ⇒ request continues.
If they don’t ⇒419 | Page Expired
(token mismatch).
Good news: for typical Blade forms you get 100 % protection by default—just remember
@csrf
.
3. Quick example – Classic Blade form
Route & controller
// app/Http/Controllers/ProfileController.php
public function updateEmail(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
]);
$request->user()->update([
'email' => $request->email,
]);
return back()->with('success', 'Email updated!');
}
View
No further work needed—the CSRF middleware will verify the token automatically.
4. CSRF with AJAX or SPA front-ends
When you post via JavaScript you must send the token manually.
Laravel exposes it in a cookie named XSRF-TOKEN
and in Blade you can also print it into a meta tag:
Axios example
Why not fetch from the
XSRF-TOKEN
cookie?
Browsers block JavaScript from reading http-only cookies.
Hence the Blade meta approach, or pull it from the DOM with@json(csrf_token())
.
5. Excluding (or including) URIs
Sometimes you need to disable CSRF for specific endpoints—e.g. a public webhook.
Edit app/Http/Middleware/VerifyCsrfToken.php
:
Conversely, if you’re building an API that uses tokens (Laravel Sanctum, Passport, etc.) you’ll often remove the CSRF middleware from your api
middleware group entirely.
6. Regenerating tokens after login / logout
Laravel automatically regenerates the session ID and CSRF token when:
-
a user logs in (
LoginController
) -
a user logs out (
LogoutController
)
This prevents session fixation and token reuse attacks.
7. Common pitfalls & fixes
Issue | Cause | Fix |
---|---|---|
**419 | Page Expired** on POST | Token missing or wrong |
Token mismatch only in Firefox or Safari | Mixed domain/sub-domain cookies | Set identical domain on session cookie or disable SESSION_DOMAIN in .env |
Testing fails with 419 | Token not included in test | In Feature tests, call withSession(['_token' => csrf_token()]) or just use $response = $this->post(route(...), $data) which automatically sets it |
SPA with Inertia / Livewire | Token out of sync after login via XHR | Refresh token from cookie/meta after axios.post('/login') succeeds |
8. Writing Feature tests with CSRF
Laravel’s test helpers handle tokens automatically unless you pass a custom $headers
array—then you must include it yourself.
9. Advanced: Changing the default header name
If you prefer another header (e.g. X-XSRF-TOKEN
instead of X-CSRF-TOKEN
) add in AppServiceProvider
:
10. Key take-aways
-
Blade forms: always include
@csrf
. -
JS/AJAX: send the token via
X-CSRF-TOKEN
header. -
Don’t disable CSRF unless you have a very good reason.
-
Regenerate tokens on sensitive auth events.
-
Test both protected & unprotected flows.
✨ That’s it! You now have a full understanding of Laravel’s CSRF protection and how to use it safely in traditional Blade apps, SPAs, and APIs.
💡 Frequently Asked Questions (FAQ) about Laravel CSRF Protection
# | Question | Short Answer |
---|---|---|
1 | What exactly is the _token field that Blade inserts? |
A hidden form input containing the CSRF token stored in the user’s session. Laravel’s middleware checks it on every state-changing request. |
2 | Do I need CSRF tokens on GET routes? |
No. Only methods that modify state (POST , PUT , PATCH , DELETE ) are checked. GET requests should remain idempotent, so CSRF is not validated there. |
3 | **Why do I get “419 | Page Expired” even though I added @csrf ?** |
4 | How do I send the token in Axios or Fetch? | Read it once from the <meta name="csrf-token"> tag and set it on every request header: axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content; . |
5 | What’s the difference between X-CSRF-TOKEN and X-XSRF-TOKEN ? |
X-CSRF-TOKEN is what you set in JS. XSRF-TOKEN is a cookie Laravel sets so that frameworks like Angular or Axios can automatically read and forward it (if you enable that feature). |
6 | Can I disable CSRF for a single webhook URL? | Yes. Add the URI to the $except array inside app/Http/Middleware/VerifyCsrfToken . |
7 | Is CSRF protection required for APIs that use Sanctum or Passport? | If you’re using token-based auth (the mobile-app style), you usually remove the CSRF middleware from api routes. If you use Sanctum’s SPA mode (cookie-based), keep CSRF enabled. |
8 | Does Livewire handle CSRF for me? | Yes. Livewire’s request payload automatically includes the token, so no manual work is needed. |
9 | Should tokens be regenerated on every request? | No. Laravel regenerates the token on login/logout and when the session ID changes. That’s sufficient; regenerating every request would break concurrent tabs. |
10 | How do I test CSRF-protected routes in PHPUnit? | Use the built-in HTTP helpers ($this->post(...) ) — they automatically attach a valid token when the test is run inside a session established with actingAs() . |
Still stuck?
-
Debug tip: Dump the current token in your view (
{{ csrf_token() }}
) and compare it with the one arriving inRequest::header('x-csrf-token')
. -
Local vs production: On localhost you can set
appVerificationDisabledForTesting
, but never disable CSRF middleware in production.