Replies: 15 comments 1 reply
-
Regarding the architectural philosophy of this proposalI would like to clarify that the core of this proposal isn't about the technical robustness of Laravel's CSRF protection—we all agree it is solid and session-based. The real issue lies in the architectural integrity of the application structure. In a standard web environment (non-API), a Currently, even though the CSRF token is valid for the session, it can be "repurposed" from a generic form to hit these exposed POST-only routes that shouldn't even be reachable without a functional UI. The core argument is that Laravel should enforce a "Resource Integrity Contract":
If a developer defines a resource but forgets to disable the |
Beta Was this translation helpful? Give feedback.
-
That's quite a lot for finding one missing line in a route file. Maybe, this would be better suited as a rector rule or the like 🤔
I don't see why this problem should exist exclusively with non-existing matching
How would that look in practice? Which HTTP response status code would you use? |
Beta Was this translation helpful? Give feedback.
-
|
Just to clarify, this isn’t really about Laravel’s CSRF protection itself CSRF is solid and works with sessions, so the token changes per session. Even if someone tried to reuse a token from another form, it would only work in the same session/browser. The real thing I’m pointing out is POST endpoints that don’t have a real form behind them. For example, if you have a resource with all the usual methods (create, store, etc.) but the GET/create route is missing or broken, then a POST request to store is probably not coming from a real user flow. For single POST-only endpoints (like
For the middleware, I’d suggest returning 403 Forbidden if it blocks a POST without a real GET/form. And of course, this middleware should be optional, so existing apps don’t break. The goal is just to add a little extra guardrail to make sure POST requests actually come from real user flows and not from “hidden” endpoints. |
Beta Was this translation helpful? Give feedback.
-
|
Just to clarify my point: the issue isn’t that every POST should always have a GET at the same URL. That only really makes sense for resources, because their pattern is clear if a resource doesn’t have a For regular POST endpoints inside web routes (like So, the suggestion is really about resources, where the flow is predictable: no |
Beta Was this translation helpful? Give feedback.
-
Why should a I mean, if you know, you always want them to come in pairs and you for some reason don't add the
Also, the fact that you are able to give use cases where people would omit a And if people don't have a form, would they then create an empty Besides, I still don't exactly see the problem here: If the CSRF is valid, why would there be a security issue in the first place? |
Beta Was this translation helpful? Give feedback.
-
|
That POST without a GET should be in API not in WEB. In anycase it should be protected by more than the CSRF. It should require auth. |
Beta Was this translation helpful? Give feedback.
-
Yeah, that's why it's not adding up. However, there's a case where a <!-- /resources/views/post.blade.php -->
<form action="/post" method="POST">
@csrf
<input type="text" name="title">
<textarea name="content"></textarea>
<input type="submit" value="Create Post">
</form><!-- /resources/views/page.blade.php -->
<form action="/page" method="POST">
@csrf
<input type="text" name="title">
<textarea name="content"></textarea>
<input type="submit" value="Create Page">
</form><!-- /resources/views/dashboard.blade.php -->
@include('post')
@include('page')<!-- /routes/web.php -->
Route::view('/', 'dashboard');
Route::post('/post', [PostController::class, 'store']);
Route::post('/page', [PageController::class, 'store']); |
Beta Was this translation helpful? Give feedback.
-
|
The 403 is not meant to blame the user. In APIs, a POST endpoint can be legitimately reached via documentation or clients. CSRF is not only about preventing cross-user attacks; it also implicitly assumes that a real form exists somewhere in the application. When a resource defines a store action without a corresponding create, that flow is structurally broken. In a resource context, create → store is an implicit contract developers rely on. In practice, missing create routes usually happen due to developer mistakes (forgotten routes, removed views, scaffolding conflicts), especially in admin registration flows. This has been repeatedly observed in real-world applications. The middleware is optional and scoped to resources only. The goal is not to enforce behavior, but to provide a guardrail against unintended, unreachable POST actions in web resources. |
Beta Was this translation helpful? Give feedback.
-
This explains why 4xx is not the proper HTTP response status code, let alone this shouldn't even be handled by status codes but by tooling as explained earlier
Adding the middleware is conscious as is adding a create route—doesn't that cancel each other out? When I always know when I consciously add said route, why would I need the middleware? |
Beta Was this translation helpful? Give feedback.
-
|
The decision may be conscious in code, but that doesn’t mean it’s always a conscious security decision. Laravel already works this way in other areas. Resources follow the same philosophy: If a developer explicitly defines a create route (even an empty one), that’s a conscious choice to bypass the default flow, and the framework should not interfere. But when store exists without create, that’s often not intentional — it’s usually a broken or incomplete resource flow caused by refactors or missing views. The middleware doesn’t second-guess conscious decisions. That’s consistent with Laravel’s approach: safe defaults, clear flows, and explicit escape hatches. |
Beta Was this translation helpful? Give feedback.
-
There's one key difference: The middleware is optional, hence opt-in. That makes bypassing it redundant |
Beta Was this translation helpful? Give feedback.
-
|
I understand your point. We can make the middleware non-optional in new versions, so it becomes a built-in part of the resource contract, ensuring that flows like create → store are properly enforced. This way, we improve security in new releases without breaking existing applications. What do you think? |
Beta Was this translation helpful? Give feedback.
-
|
I still think this should be solved differently. |
Beta Was this translation helpful? Give feedback.
-
|
I think there could be another solution: have a guardrail in the Laravel core that ensures the resource flow is correct (create → store, edit → update). This would apply to new versions, so existing applications won’t break. If a developer doesn’t want this behavior, they could override or opt out, keeping flexibility while improving security by default. |
Beta Was this translation helpful? Give feedback.
-
|
Optional middleware or a package would be redundant—developers aware of the resource flow already handle create → store correctly. Having it in core acts as a guardrail for accidental misconfigurations, while still allowing conscious overrides. This improves security in new versions without affecting existing apps and aligns with Laravel’s philosophy of safe defaults and explicit escape hatches |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Laravel Version
12
PHP Version
8.2
Database Driver & Version
No response
Description
Many Laravel applications have POST-only routes, especially for resource controllers (e.g., admin registration), without providing the corresponding GET (
create) route or form.This creates a potential security risk: an attacker could use a CSRF token from a completely unrelated form to submit to the POST route, performing unintended actions like creating admin accounts.
Resource controllers usually avoid this issue because
storenormally has a correspondingcreateGET route. However, in custom POST endpoints or scaffolded routes generated bylaravel/ui, the GET form may be missing, leaving the POST route exposed.Proposed Solution
Introduce an optional middleware that ensures POST requests are allowed only if the corresponding GET route exists and is functional (HTTP 200).
This approach prevents CSRF misuse while remaining developer-friendly and backward-compatible.
Steps To Reproduce
laravel/ui, for example an admin registration resource.createroute for this resource, leaving only the POST (store) route active.admin/registerPOST route reusing the CSRF token from the unrelated form.createroute exists and is functional (returns 200).Notes
storehas a correspondingcreateGET route.laravel/uiwhere GET forms may be missing.Beta Was this translation helpful? Give feedback.
All reactions