~/learn / how-apps-are-built / mvc-and-middleware

MVC, Routing, and Middleware

How the model-view-controller pattern, routers, and middleware chains structure a web app, why middleware order and path normalization create bugs, and the authorization gaps each layer commonly leaves.

intermediateupdated

Model-view-controller (MVC) is the dominant structure for server-side web frameworks: Rails, Django (which calls it MTV), Laravel, Spring MVC, and ASP.NET MVC all follow it.

  • Models hold data and business rules, usually mapping to database tables.
  • Views render output, as HTML templates or JSON serializers.
  • Controllers receive requests, coordinate models, and choose a view.

The value of recognizing this structure is that it tells you where each kind of check belongs, and therefore where to look when one is missing.

Routing

The router maps an HTTP method and path to a controller action. Convention-based routing generates many routes from one declaration:

# Rails: one line creates index, show, new, create, edit, update, destroy
resources :invoices

Security angle: convention can expose actions nobody meant to publish. If the team only built a page for viewing invoices, are update and destroy still routed and reachable? In some frameworks every public method on a controller is callable, so a helper method accidentally becomes an endpoint. Dumping the route table (bin/rails routes, php artisan route:list, rails/info/routes) is one of the fastest ways to find forgotten endpoints in a review, and reading the frontend JavaScript bundle finds the ones the UI knows but you have not clicked.

Middleware, and why order is a vulnerability

Middleware wraps every request in a chain of layers, each able to inspect, modify, short-circuit, or pass the request along:

request -> logging -> sessions -> CSRF -> authentication -> router -> controller

Order is load-bearing. Authentication that runs after a route has already produced a response protects nothing. A rate limiter placed after the expensive work it was meant to throttle is decorative. A body-parsing middleware placed before a size limit lets an oversized body exhaust memory before the limit applies. When reviewing, read the chain in order and ask what each layer assumes the previous ones already did.

Opt-outs are where the holes are. Frameworks let a specific route skip a middleware (Rails skip_before_action, an Express route that bypasses the auth middleware). Every opt-out is a deliberate hole that needs an independent justification, and a webhook endpoint that skipped CSRF had better authenticate some other way.

Path normalization mismatches are a subtle, high-value class. A middleware rule that guards /admin may not match /Admin, /admin/, /admin%2f, /admin;x, or /./admin if the middleware and the router normalize paths differently. The request sails past the guard and still reaches the protected handler. This same disagreement between two parsers, seen here between a proxy and an app, is what causes request smuggling and many WAF bypasses.

Where authorization lives

Authentication answers “who are you” and usually lives in middleware. Authorization answers “may you do this to this object” and usually cannot live in middleware, because it depends on the specific record being accessed.

# Vulnerable: any logged-in user reads any invoice by changing the id
def show
  @invoice = Invoice.find(params[:id])
end

# Safe: scope the lookup to what the user owns, so a foreign id simply is not found
def show
  @invoice = current_user.invoices.find(params[:id])
end

The safe version is powerful because it makes the authorization impossible to forget: the query itself cannot return a record the user does not own. Authorization libraries (Pundit, CanCanCan, Django permissions, Laravel policies) centralize these checks, and their real value is a mechanism that fails the request when an action never called authorize at all, turning “forgot to check” from silent exposure into a loud error.

Validation layers

Input can be validated in the browser, the controller, the model, and the database, but only server-side checks count for security, and the strongest guarantees are the ones closest to the data. Browser validation is a UX convenience an attacker skips entirely. Controller and model validation catch most bad input. Database constraints (NOT NULL, unique indexes, foreign keys, check constraints) hold no matter which code path, job, or console session writes the row, which is why a unique index is a more reliable “redeem once” guarantee than any application check (see business logic).

Field notes: attacking the routing and middleware layer

The seams between the router, the middleware stack, and the controller are where access control quietly fails:

  • Path normalization mismatches. A gateway, WAF, or auth middleware inspects the raw path, but the framework normalizes it differently before routing. /admin/..;/dashboard, /admin/%2e%2e/, /%2561dmin (double-encoded), a trailing dot /admin., or /admin/./ can slip past a prefix-based guard and still reach the protected handler. Try adding ;, ..;, encoded slashes, and trailing characters to any blocked path.
  • Middleware ordering bugs. If logging, body-parsing, or a rewrite runs before authentication, you can reach behavior that should have been gated. If authorization is a decorator the developer forgot on one action, that action is open. Enumerate every route and test each without credentials.
  • Verb tampering. A route protected for GET and POST may be reachable via PUT, PATCH, HEAD, or OPTIONS, or via X-HTTP-Method-Override. Frameworks map verbs to actions unevenly.
  • Debug and framework routes. /rails/info/routes, Django’s debug page (DEBUG=True leaks settings and a console), Actuator endpoints (/actuator/env, /actuator/heapdump) in Spring Boot, and .env files exposed at the web root are all one request away and devastating. Fuzz for them.
  • Resourceful route expansion. Rails resources :users and its equivalents generate the full CRUD set. If the UI only links to index and show, the destroy and update actions still exist; call them directly.

Test only applications you own or are authorized to assess.

Review checklist

  • List every route and confirm each one is intended and reachable only by whom it should be.
  • Find every place a middleware or filter is skipped, and justify each.
  • Confirm the middleware order puts authentication, CSRF, and limits before the work they protect.
  • For every action that loads a record by id, confirm the lookup is scoped to the current user or authorized explicitly.
  • Confirm API controllers and web controllers enforce the same rules, since they are two doors to the same rooms.