0% прочитано

The Middleware Was Correct. The Boundary Was Wrong.

A security bug does not always mean the middleware is wrong. Sometimes the rule is correct for one user flow, while the endpoint accidentally lives inside another authorization boundary.

21 жовтня 2026 р. 10 хв читанняLaravel

Recently I investigated a security bug that initially looked like a middleware problem.

The verification rule was completely valid for the public or cabinet area: some actions there really should be available only after email verification.

But the admin UI reused the same endpoints. As a result, an administrative flow accidentally passed through middleware designed for a different authorization boundary.

The middleware was not wrong. The boundary was.

When middleware blocks a legitimate flow, I first ask not "how do I bypass it?" but "does this endpoint actually belong to this route boundary?"

A Correct Security Rule Can Be Wrong in Another Context

Two flows have different access rules
Public / cabinet flow authenticated    |    vemail verified    |    vallowed action  Admin flow authenticated admin    |    v

Both rules can be correct at the same time.

A cabinet flow may depend on email verification, while admin access depends on an administrative role or permission.

The problem appears when one endpoint is reused by both flows but still carries middleware from only one of them.

A Route Group Often Becomes a Security Boundary

The middleware stack defines more than a technical pipeline
Route::middleware([    'auth',    'verified',])->group(function (): void {    Route::get(        '/profile/resources',        ResourceIndexAction::class,    );});

It is easy to see a route group only as a way to avoid repeating auth, a prefix, or a controller namespace.

From a security perspective, it also defines the preconditions for every endpoint inside it.

If an endpoint lives in that group, it effectively accepts the group access rules.

A Shared Endpoint Does Not Always Mean a Shared Authorization Policy

Reusing an endpoint looks attractive: fewer routes, fewer controllers, one implementation.

But when public and admin flows have different access rules, a shared endpoint starts accumulating conditional exceptions.

At some point the code can no longer answer a simple question: "who is allowed to call this action?"

A Role-Specific Middleware Bypass Is a Symptom

A local bypass can hide the wrong route boundary
if ($user->isAdmin()) {    return $next($request);} if (! $user->hasVerifiedEmail()) {    abort(403);}

This kind of fix is sometimes necessary, but for me it is also a signal to stop and inspect the architectural boundary.

If middleware starts knowing every role and exception in the system, it may already be serving several different security contexts at once.

I Separate Authentication, Verification, and Authorization

This case is closely related to the article about the difference between authentication and authorization.

Authentication answers who the user is. Verification confirms an additional account state. Authorization decides whether the user may perform a specific action.

These checks may appear next to each other in a pipeline, but they are not interchangeable.

Admin Access Should Not Accidentally Inherit Cabinet Prerequisites

Different flows get their own prerequisites
Wrong boundary shared endpoint    |    +-- auth    +-- verified    +-- maybe admin permission  Better boundaries cabinet endpoint    |    +-- auth    +-- verified    +-- cabinet policy

This does not mean the business logic has to be duplicated.

Routes or action classes can be separate while both flows call the same application service underneath.

I separate the transport and security boundary from reuse of the business operation.

One Service Can Be Shared Even When Endpoints Are Separate

Security boundaries are separate while application logic is shared
final class CabinetResourceAction{    public function __invoke(        Request $request,        ResourceService $service,    ) {        $this->authorize(            'viewFromCabinet',        );         return $service->handle(            $request->user(),        );    }} 

Route Reuse and Code Reuse Are Not the Same Thing

This was the key distinction for me in this case.

A desire to avoid code duplication does not mean every context needs one URL and one middleware stack.

The query, service, or DTO can be reused while entry points remain separate with their own security rules.

I Define the Boundary Before Choosing Middleware

Before adding middleware, I now ask several questions:

  • Who is the actor for this endpoint?
  • Is this a public, cabinet, or admin flow?
  • Which prerequisites really belong to this flow?
  • Is verification actually required for this action?
  • Which permission or policy protects the business action?
  • Is the endpoint reused from another security context?

Tests Should Represent Different Security Contexts

The test matrix represents boundaries, not one happy path
Cabinet user  authenticated + unverified -> blocked  authenticated + verified   -> allowed  Admin user  admin permission present    -> allowed  admin permission missing    -> blocked

If only the cabinet endpoint is tested, an admin regression may remain invisible.

Conversely, a test that only checks "admin gets 200" without the permission boundary may hide overly broad access.

Tests should capture not only the result but also why access is allowed or denied.

Sometimes the Security Fix Is Moving the Endpoint, Not Changing Middleware

This is the most important part of the case.

If verification middleware correctly protects the cabinet group, weakening it just to make the admin UI work may be the wrong fix.

It may be safer to move the admin entry point into its own route group and keep both boundaries explicit.

My Checklist When Middleware Blocks a Legitimate Flow

  • I check which user flow the middleware was originally designed for.
  • I inspect who else is using the endpoint.
  • I separate account prerequisites from action authorization.
  • I inspect the route group, prefix, and middleware stack.
  • I treat role-specific bypasses as a possible sign of the wrong boundary.
  • I check whether entry points can be separated while keeping a shared service.
  • I add separate security tests for each context.

Middleware Enforces a Rule Inside a Boundary, but Does Not Prove the Boundary Is Correct

After this case, I am much more careful about treating security middleware as the final answer.

Middleware can be completely correct for its context and still break another flow if that flow accidentally enters the same route group.

So when an access rule looks wrong, I inspect not only the condition. I inspect whether the endpoint sits on the correct authorization boundary.

Sometimes the security fix is not to weaken middleware. It is to move the endpoint back to the boundary where it actually belongs.