PyFu

FastAPI Middleware

Python Web Development Frameworks

Middleware in FastAPI is a mechanism that allows you to process requests and responses globally before they reach your route handlers or after your route handlers generate a response. Middleware runs for every request that comes into the application, regardless of the endpoint being accessed.

Middleware can be used for a wide range of cross-cutting concerns such as logging, authentication, request/response modification, error handling, security headers, rate limiting, and more.

Define a FastAPI Middleware

To define middleware in FastAPI, you typically use either the @app.middleware decorator or add reusable middleware classes using add_middleware().

Using the decorator approach allows you to write simple inline middleware functions. The function receives the request, passes it to the next middleware or route handler by calling call_next(), and can modify the response before returning it.

Here is a basic example of inline middleware using the decorator:

from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    response = await call_next(request)
    response.headers["X-App-Version"] = "1.0"
    return response

In this example, every HTTP response from the server will automatically include a custom header called X-App-Version.

For more advanced or reusable middleware, you can define a middleware class. This is the preferred way if you are building middleware that might be shared across multiple projects or needs to be more configurable. A class-based middleware subclasses BaseHTTPMiddleware and implements dispatch(), which receives the request and the call_next callable:

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

class AuthMiddleware(BaseHTTPMiddleware):
    PUBLIC = ("/login", "/docs", "/openapi.json")

    async def dispatch(self, request, call_next):
        if request.url.path.startswith(self.PUBLIC):
            return await call_next(request)
        user = verify_token(request.headers.get("authorization", ""))
        if user is None:
            return JSONResponse({"detail": "unauthorized"}, status_code=401)
        request.state.user = user
        return await call_next(request)

app.add_middleware(AuthMiddleware)

This middleware enforces authentication for the whole application from one place: it lets a small allowlist of paths through, rejects anything else without a valid token, and stashes the resolved user on request.state for handlers to read. It looks airtight. It is not, and the reasons it fails are the same reasons middleware is worth attacking.

Why middleware matters from an offensive security perspective

Middleware is the one piece of code that runs on every request before any handler, which makes it the most efficient place in the app to put a security control and the most efficient place to break one. When I assess a FastAPI app, middleware is the first layer I map, because a single flaw here is not one vulnerable endpoint, it is the entire API surface at once. The AuthMiddleware above is the archetype: the auth model rides on three lines, and three lines are easy to get wrong.

The way to find AuthN/Z issues in middleware is to separate two questions and answer them for every middleware in the stack: who establishes identity, and who enforces it. Identity is token extraction, signature checks, request.state.user population. Enforcement is the code that actually returns a 401/403 or short-circuits. The bugs live in the gap between them, and these are the patterns I look for:

  • Path-gated enforcement is the flagship bug. Any middleware that decides “is this request allowed” from request.url.path is gating on a string the attacker controls. The startswith(self.PUBLIC) check above is bypassable in both directions: startswith on a tuple means /login-as-admin and /docs-internal/secrets are treated as public because they share a prefix, and path tricks like /admin/../login, a trailing slash, mixed case, doubled slashes //admin, or %2e%2e segments can make the normalized route differ from the string the middleware inspected. Map the allowlist, then probe every sensitive route for a prefix or normalization that slips it into the public set.

  • Registration order is execution order, and it is inverted. add_middleware stacks LIFO, so the last middleware added is the outermost wrapper and runs first. An auth middleware added before a middleware that can return a response early (a cache layer, a CORS preflight short-circuit, a custom error handler) can be skipped entirely for the requests that other layer answers. Write down the order, then test the request shapes that hit the short-circuits, OPTIONS preflights and HEAD first.

  • The early-return and exception paths. Middleware enforcement is only as good as its worst code path. A dispatch that wraps call_next in a try/except and returns a generic response on error can convert a crash inside auth into a fail-open. A branch that return await call_next(request) before the token check, added later for one “internal” case, is a permanent hole. Read every path through dispatch and ask which ones reach call_next without passing enforcement.

  • Middleware cannot use Depends, so it reinvents auth badly. Middleware runs outside route resolution and the dependency-injection graph, so it has no access to the matched route’s security scopes and usually reimplements token parsing by hand. That hand-rolled parser is where I find accepted alg=none, unverified signatures, and missing expiry checks, the same primitives covered in JWT for Authentication and Authorization in FastAPI. When an app enforces auth in both middleware and per-route FastAPI Dependency Injection, diff them: the two often disagree about what is public, and the looser of the two wins.

  • Trusting forwarded headers. Middleware that makes decisions from X-Forwarded-For, X-Forwarded-Host, X-Real-IP, or environment headers is trusting values a client can set unless a proxy strips them. This is the same class as Authentication Bypass via Development Environment Headers Abuse, and it shows up in rate-limiters keyed on a spoofable IP and in “internal only” gates that check a header instead of a network boundary.

  • request.state is a shared mutable bag. The middleware sets request.state.user; the handler reads it. If a handler anywhere reads identity from somewhere else, a query param, a header, a re-parsed token, the carefully enforced middleware value is irrelevant for that route. Trace where request.state.user is set and confirm every handler trusts that and only that.

  • CORS and security headers applied on the wrong path. CORSMiddleware with allow_origins=["*"] together with allow_credentials=True, or an origin reflected from a loose regex, turns the browser into a confused deputy. Separately, security headers added only on the success branch of a middleware vanish on error responses, which are exactly the responses that leak stack traces.

When I read a middleware stack I am really building a map of which requests reach a handler having passed identity and enforcement, and which reach it having skipped one of them. Every gap in that map, a path the allowlist misclassifies, a method the order lets through, an exception that fails open, a header the app trusts, is broken access control delivered globally instead of one endpoint at a time. That breadth is exactly why middleware is worth the time: get past it once and the rest of the API is downstream. The object-level checks that should still catch you afterward, and usually do not, are in Business Logic Vulnerabilities in FastAPI Applications and Broken Access Control in Flask Applications. Defenders should keep authorization decisions in route dependencies where they can see the matched route and its scopes, treat any path-string gate as a parser to be fuzzed, and never let middleware be the only thing standing between a request and an object it should not touch.

Mitigation

Use middleware for cross-cutting concerns that are not the final authorization decision, and put the authorization itself in FastAPI Dependency Injection, where the matched route and its scopes are visible and the dependency can fail closed by raising. When middleware must gate access, never decide from the raw request.url.path string or from forwarded headers; match against the resolved route or scope, and treat any code path that reaches call_next without enforcement as a bug. If you do need a global guard, attach it as a dependency on the app or router rather than reimplementing token parsing by hand in dispatch.

from fastapi import FastAPI, Depends

# Identity is resolved in middleware; the binding enforcement is a
# dependency so it sees the route, fails closed, and cannot be skipped.
def require_user(request: Request):
    user = getattr(request.state, "user", None)
    if user is None:
        raise HTTPException(status_code=401, detail="unauthorized")
    return user

app = FastAPI(dependencies=[Depends(require_user)])