PyFu

FastAPI Dependency Injection

Python Web Development Frameworks

FastAPI uses dependency injection as a core mechanism to share data and functionality across an application. You declare certain parameters as dependencies with Depends, and FastAPI resolves and provides them when the endpoint is called. It keeps code modular and testable, and it is the idiomatic place to put reusable logic like database sessions, settings, and, most importantly for us, authentication and authorization.

That last point is why dependency injection matters from an offensive perspective. In a typical FastAPI app, dependency injection is the access-control layer. There is no separate middleware enforcing per-route permissions, the auth check is a dependency attached to the endpoint. So when you audit a FastAPI app, the dependency graph is the attack surface: a route’s security is decided entirely by which dependencies it declares and whether those dependencies actually enforce anything.

How to define a dependency

A dependency is any callable (function, class, or lambda) that returns a value, declared with Depends. FastAPI calls it and passes the result to your endpoint:

from fastapi import Depends, FastAPI

app = FastAPI()

def common_parameters(q: str = None):
    return {"q": q}

@app.get("/items/")
def read_items(commons: dict = Depends(common_parameters)):
    return commons

Dependencies can depend on other dependencies, and FastAPI caches each one per request, so a security dependency used by several sub-dependencies runs once. A dependency that does not return a value can still run for its side effects, which is how auth guards are usually written:

@app.get("/admin", dependencies=[Depends(require_admin)])
def admin_panel():
    return {"ok": True}

Here require_admin produces no value the endpoint uses; it exists purely to raise HTTPException if the caller is not an admin. This is the pattern to scrutinize.

Dependency injection as a security control

A correct security dependency does two things: it resolves the caller’s identity, and it raises when the caller fails the check. The endpoint only runs if the dependency returned without raising.

def get_current_user(authorization: str = Header(...)) -> dict:
    user = resolve_token(authorization)
    if user is None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return user

@app.get("/me")
def me(user: dict = Depends(get_current_user)):
    return user

Because this guard runs before the handler body, it is genuinely enforced, provided every protected route actually declares it. That “provided” is where things break.

Where it goes wrong

1. The dependency is simply not applied. The most common failure is also the dumbest: a sensitive route omits the auth dependency that its siblings have. FastAPI does not enforce anything globally unless you tell it to, so an unguarded route is wide open. This is the FastAPI flavor of broken access control and is trivially found by listing every route and checking which ones lack a security dependency.

2. The dependency fails open. A guard that returns on failure instead of raising enforces nothing, because FastAPI only blocks the request when the dependency raises:

def get_current_user(authorization: str = Header(None)) -> dict | None:
    user = resolve_token(authorization)
    if user is None:
        return None        # BUG: should raise HTTPException(401)
    return user

@app.get("/admin")
def admin(user: dict = Depends(get_current_user)):
    # user is None for an unauthenticated caller, but the handler still runs
    return {"secret": "..."}

The handler receives user=None and proceeds. Unless every handler re-checks for None (and they never all do), an unauthenticated request sails straight through. Always verify the guard raises; a security dependency that can return a falsy value is a red flag.

3. The dependency authenticates but does not authorize. A guard that confirms who you are but not what you may access leaves object-level checks to the handler, and handlers forget. A route like GET /profile/{user_id} that depends on get_current_user but never compares current_user.id to user_id is authenticated and still an IDOR, exactly the pattern in Business Logic Vulnerabilities in FastAPI Applications.

4. Attacker-influenced sub-dependencies. Dependencies receive request parameters too. If a security-relevant dependency derives a decision from a value the attacker controls (a path param, a header, a query arg) without validating it, the dependency itself becomes the bypass. Trace what each guard reads and ask whether the caller can shape it.

What to check when auditing

  • Enumerate every route and map the dependencies attached to each; flag any sensitive route missing the auth guard.
  • Read each security dependency and confirm it raises on failure rather than returning None/False.
  • Confirm guards do object-level authorization, not just authentication.
  • Look for dependencies=[...] declared at the app/router level (global guards) and verify nothing is excluded by accident, and that the global guard is not silently overridden.

The takeaway: in FastAPI, dependency injection is not just plumbing, it is the security perimeter. A missing Depends, a guard that returns instead of raises, or a guard that authenticates without authorizing are the three highest-yield findings in any FastAPI assessment. The lab app fastapi-fu/fastapi-di-auth-bypass demonstrates the fail-open guard end to end; the correct enforcement pattern lives in FastAPI HTTP Basic Auth and JWT for Authentication and Authorization in FastAPI.

Why dependency injection matters from an offensive security perspective

When I audit a FastAPI app, the dependency graph is the first thing I draw, because in this framework dependency injection is the access-control layer. There is rarely a separate component enforcing per-route permissions; a route is exactly as secure as the dependencies it declares, and no more. That makes a missing Depends indistinguishable from a deliberate public endpoint until you read the code, and it makes the guard functions themselves high-value targets. These are the tells I hunt for:

  • A sensitive route missing the auth dependency its siblings have. FastAPI enforces nothing globally unless told to, so one route that omits the guard is wide open while everything around it looks protected. I enumerate every route and diff the dependency list against the sensitive set.
  • A guard that returns instead of raises. FastAPI only blocks a request when a dependency raises. A guard that returns None/False on failure fails open, and the handler runs with no principal. Any security dependency that can produce a falsy value is a finding.
  • Authentication without authorization. A guard that confirms who you are but never checks what you may touch leaves object-level checks to handlers that forget them, which is how GET /profile/{user_id} becomes an IDOR.
  • Attacker-influenced sub-dependencies. Dependencies receive request parameters too. A guard that derives its decision from a path param, header, or query value the caller controls is a guard the caller can bend.

For a defender the rule is simple: treat the dependency layer as the security perimeter, make every guard fail closed, and confirm authorization happens, not just authentication.

Proof of exploitation

Run the lab app (PyFuLabs/fastapi-fu/fastapi-di-auth-bypass). The auth dependency returns None instead of raising when no token is supplied, and the handler treats None as acceptable, so the protected route runs unauthenticated:

curl -s "http://pyfu.local/fastapi-fu/fastapi-di-auth-bypass/api/admin"
{"user":null,"secret":"TOP SECRET admin data - flag{di_fail_open}","all_users":["admin"]}

The fail-closed variant, whose dependency raises on missing credentials, returns 401 for the same request.

Mitigation

The fix is to make authentication dependencies fail closed. A dependency that returns None on a missing or invalid credential lets the request proceed with no user, so it must instead raise HTTPException(status_code=401) whenever authentication fails, and the route should never treat user is None as an acceptable state. Depend on a function that returns a guaranteed-valid principal or raises, and reject the fail-open pattern entirely.