What Is Cross-Site Request Forgery (CSRF)?
An attack where a malicious website tricks a user's browser into performing an unwanted action on a site where the user is authenticated. The browser automatically sends cookies with the request, so the target site processes it as a legitimate action from the user.
How It Works
CSRF exploits the browser's automatic cookie-inclusion behavior to make a victim's browser perform an authenticated action on a target site without the victim's knowledge.
Step 1: Victim authentication — The victim logs into yourapp.com. The server sets a session cookie (laravel_session=abc123) in the browser. The browser stores this cookie and will include it automatically with every future request to yourapp.com — including requests initiated by other websites.
Step 2: Attacker preparation — The attacker creates a malicious page at evil.com containing a form that targets your application: <form action="https://yourapp.com/account/email" method="POST"><input name="email" value="attacker@evil.com"></form>. An inline script auto-submits the form when the page loads.
Step 3: Social engineering — The attacker sends the victim a link to evil.com — disguised as an interesting article, a prize notification, or a support link.
Step 4: Cross-origin request — The victim visits evil.com while still logged into yourapp.com. The malicious form submits automatically. The browser constructs a POST request to https://yourapp.com/account/email and, per its normal behavior, includes the laravel_session cookie for yourapp.com.
Step 5: Server processing — yourapp.com receives a POST request to /account/email with a valid session cookie. Without CSRF protection, the server has no way to distinguish this request from one the victim intentionally made — it processes the email change.
CSRF token defense — Laravel generates a unique, unpredictable token per user session and stores it server-side. When @csrf is included in a form, the token is embedded as a hidden _token field. On submission, VerifyCsrfToken middleware compares the submitted token against the session's stored token. The attacker's form at evil.com cannot read the victim's CSRF token (blocked by the same-origin policy) and cannot include it — so the request is rejected with a 419 status.
Types of Cross-Site Request Forgery (CSRF)
GET-Based CSRF
State-changing action triggered via a GET request (bad practice but it exists) — exploited with an `<img>` tag or link that the victim simply views or hovers.
<!-- Attacker embeds in any page the victim might visit -->
<img src="https://yourapp.com/account/delete?confirm=true" style="display:none">
POST-Based CSRF
Hidden form auto-submitted via JavaScript when the victim loads the attacker's page, targeting a POST endpoint on the victim's authenticated site.
<form id="csrf" action="https://yourapp.com/account/password" method="POST">
<input name="password" value="attacker_new_pass">
<input name="password_confirmation" value="attacker_new_pass">
</form>
<script>document.getElementById("csrf").submit();</script>
Login CSRF
Attacker forces the victim to log in as the attacker's account — if the application then stores data tied to the session (search history, payment methods), the attacker can later log in and retrieve that data.
<!-- Force victim to log in as attacker -->
<form action="https://yourapp.com/login" method="POST">
<input name="email" value="attacker@evil.com">
<input name="password" value="attackerpassword">
</form>
JSON-Based CSRF
API endpoints that accept JSON bodies may be targeted if they rely only on session cookies for authentication and CORS is misconfigured to allow cross-origin requests with credentials.
fetch('https://yourapp.com/api/account', {
method: 'POST',
credentials: 'include', // Sends session cookie cross-origin
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
});
In Laravel Applications
Laravel includes built-in CSRF protection via the VerifyCsrfToken middleware. Every form must include the @csrf Blade directive, and AJAX requests must send the X-CSRF-TOKEN header. The protection works by generating a unique token per session and validating it on every POST, PUT, PATCH, and DELETE request.
Code Examples
Form Without vs. With CSRF Token
Vulnerable
{{-- VULNERABLE: No CSRF token — any site can submit this form --}}
<form action="/account/email" method="POST">
<input type="email" name="email" value="{{ $user->email }}">
<button type="submit">Update Email</button>
</form>
{{-- Server processes this form even when submitted from evil.com --}}
Secure
{{-- SECURE: @csrf generates a hidden _token input field --}}
<form action="/account/email" method="POST">
@csrf
{{-- Renders as: <input type="hidden" name="_token" value="unique_token"> --}}
<input type="email" name="email" value="{{ $user->email }}">
<button type="submit">Update Email</button>
</form>
{{-- VerifyCsrfToken middleware validates the token on every POST/PUT/PATCH/DELETE --}}
AJAX Request Without vs. With CSRF Header
Vulnerable
// VULNERABLE: AJAX request without CSRF token
// VerifyCsrfToken will reject this with 419 Page Expired
fetch('/api/account/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'new@example.com' })
});
Secure
// SECURE: Read token from meta tag and include in every AJAX request
// Add to your HTML <head>: <meta name="csrf-token" content="{{ csrf_token() }}">
// Option 1: Manual header inclusion
fetch('/api/account/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ email: 'new@example.com' })
});
// Option 2: Axios global config (reads X-XSRF-TOKEN cookie automatically)
// axios.defaults.withCredentials = true; // Axios handles CSRF automatically
Real-World Example
Without @csrf, an attacker could create a form on their site that submits a POST request to your Laravel app's /account/delete endpoint. If a logged-in user visits the attacker's page, their account would be deleted without their knowledge.
Why It Matters
CSRF attacks work because browsers automatically include cookies with every request to a domain. A user browsing a malicious website while logged into your Laravel application has all their session cookies available for cross-site requests. Without CSRF protection, any form submission or state-changing action can be triggered silently from another website.
Laravel's CSRF protection via VerifyCsrfToken middleware is comprehensive for web applications, but developers frequently encounter issues in two specific scenarios: AJAX requests and mobile API endpoints. AJAX requests must include the X-CSRF-TOKEN header set from the csrf_token() meta tag. API routes in routes/api.php are typically excluded from CSRF protection (they use stateless token authentication instead), but mixing API and web routes in routes/web.php without understanding this distinction creates gaps.
The $except array in App\Http\Middleware\VerifyCsrfToken is a common source of CSRF vulnerabilities. Developers add routes to $except to fix broken integrations (webhooks, payment callbacks) and sometimes add overly broad patterns like /payment/* that exclude more routes than intended.
Single-page applications (SPAs) using Laravel Sanctum require specific CSRF cookie handling via the /sanctum/csrf-cookie endpoint. Misconfiguring this is a frequent source of CSRF vulnerabilities in React or Vue frontends backed by Laravel APIs.
Common Misconceptions
Myth: Our API uses JWT tokens, so we do not need to worry about CSRF.
Reality: JWT tokens stored in localStorage are not sent automatically by the browser, so they are immune to CSRF. But JWT tokens stored in cookies ARE vulnerable to CSRF and need protection.
Myth: Adding @csrf to forms is enough — AJAX calls are protected automatically.
Reality: AJAX calls must explicitly include the CSRF token via the X-CSRF-TOKEN or X-XSRF-TOKEN header. This requires reading the token from the meta tag or cookie and attaching it to each request, which does not happen automatically.
Myth: CSRF protection is handled by Laravel automatically for all routes.
Reality: Only routes in `routes/web.php` get CSRF protection by default. Routes in `routes/api.php` are excluded. Check `App\Http\Kernel.php` `$middlewareGroups` to see exactly which middleware applies to which route group.
How to Detect This
Check `app/Http/Middleware/VerifyCsrfToken.php` for the `$except` array — any route listed there is unprotected against CSRF. Verify CSRF tokens are present in all HTML forms by viewing page source on your key forms and searching for `_token` input fields. Use browser developer tools to inspect the Network tab when submitting forms and verify the `X-CSRF-TOKEN` or `_token` parameter is sent. For AJAX-heavy applications, search your JavaScript files with `grep -rn "X-CSRF-TOKEN\|X-XSRF-TOKEN" resources/js/` to verify headers are being sent. StackShield validates that your session cookie settings are correctly configured with `HttpOnly` and `SameSite` attributes that complement CSRF protection.
How to Prevent This in Laravel
-
1
Include `@csrf` in every HTML form that performs a state-changing action — this is mandatory for POST, PUT, PATCH, and DELETE forms in `routes/web.php`.
-
2
Review `app/Http/Middleware/VerifyCsrfToken.php` and verify the `$except` array contains only webhook endpoints (payment providers, third-party callbacks) and nothing else.
-
3
Set `SameSite=Lax` (or `Strict`) on your session cookie in `config/session.php` with `'same_site' => 'lax'` — this provides browser-level CSRF protection as a defense-in-depth layer.
-
4
For SPA applications using Laravel Sanctum, call `/sanctum/csrf-cookie` before making authenticated requests and configure `SANCTUM_STATEFUL_DOMAINS` in `.env` to list your frontend's domain.
-
5
Verify AJAX requests include the CSRF token by searching your JS files: `grep -rn "X-CSRF-TOKEN\|X-XSRF-TOKEN" resources/js/` — if empty, configure Axios to send the token automatically.
Frequently Asked Questions
Does Laravel's @csrf directive protect all routes automatically?
The `@csrf` directive generates a token input, and `VerifyCsrfToken` middleware validates it — but only for routes in the `web` middleware group (`routes/web.php`). Routes in `routes/api.php` are in the `api` middleware group which does NOT include CSRF verification by default. This is intentional — API routes use stateless token authentication (Sanctum, Passport) instead of session cookies. Verify this in `app/Http/Kernel.php` by checking `$middlewareGroups`.
How do I send CSRF tokens in AJAX requests with Laravel?
Add `<meta name="csrf-token" content="{{ csrf_token() }}">` to your HTML `<head>`. In JavaScript, read this value with `document.querySelector('meta[name="csrf-token"]').content` and include it as the `X-CSRF-TOKEN` header in fetch/XMLHttpRequest calls. If you use Axios, it reads the `XSRF-TOKEN` cookie that Laravel sets and sends it as the `X-XSRF-TOKEN` header automatically — you just need to ensure `withCredentials: true`.
Does the SameSite cookie attribute replace the need for CSRF tokens?
`SameSite=Strict` prevents the session cookie from being sent on any cross-origin request, which would prevent all CSRF attacks. However, it also prevents the cookie from being sent when users click links to your site from emails or other domains, breaking legitimate navigation. `SameSite=Lax` (Laravel's default) prevents cross-origin POST/PUT/DELETE but allows cross-origin GET requests. Use both `SameSite=Lax` and CSRF tokens together — the SameSite attribute is a defense-in-depth layer, not a complete replacement.
Are API routes in Laravel vulnerable to CSRF?
Stateless API routes using Bearer token authentication (JWT, Sanctum token) are not vulnerable to CSRF because Bearer tokens are not sent automatically by browsers — the JavaScript must explicitly include them in the `Authorization` header. CSRF attacks only work when the browser automatically sends credentials (session cookies). API routes that use session-based authentication (Sanctum SPA mode with cookies) ARE vulnerable to CSRF and require the Sanctum CSRF cookie mechanism with proper `SANCTUM_STATEFUL_DOMAINS` configuration.
Related Terms
Cross-Site Scripting (XSS)
A vulnerability where an attacker injects malicious JavaScript into web pages viewed by other users. The injected script runs in the victim's browser with the same privileges as legitimate scripts, allowing the attacker to steal session tokens, redirect users, or modify page content.
Session Hijacking
An attack where an adversary takes over a valid user session by stealing or predicting the session identifier. Once the attacker has the session ID, they can impersonate the user and perform any action the user is authorized to do.
Security Headers
HTTP response headers that instruct browsers how to handle your website's content securely. They protect against common attacks like cross-site scripting (XSS), clickjacking, and protocol downgrade attacks by telling the browser what actions are allowed.
Monitor Your Laravel Application's Security
StackShield continuously checks your Laravel application from the outside, catching security issues before attackers find them.
Start Free Trial