Safeguard
Regulatory Compliance

FastAPI and Pydantic dependency injection security pitfalls

FastAPI's Depends() and Pydantic's type validation look airtight but hide real bypass patterns — caching bugs, extra="allow" mass assignment, and leaked test overrides.

Aman Khan
AppSec Engineer
7 min read

FastAPI's dependency injection system is the framework's signature feature — the Depends() pattern that wires authentication, database sessions, and request validation into clean, declarative code. It's also where a surprising number of production security failures originate. FastAPI dependency injection security issues rarely stem from FastAPI itself; they stem from assumptions developers make about when Pydantic validates, what a dependency actually guarantees, and how override chains behave in test versus production code. In audits of FastAPI services since Pydantic v2 shipped in June 2023, we've repeatedly found the same handful of patterns: dependencies that look like they enforce authorization but silently no-op, Pydantic models that appear strict but coerce or skip validation on edge-case input, and shared state leaking between requests through improperly scoped dependencies. This post walks through the five most common failure modes and what to do about each.

What Is FastAPI Dependency Injection Security, and Why Does It Fail in Practice?

FastAPI dependency injection security fails most often not because a dependency is missing, but because a dependency is present and developers assume it's doing more than it is. Depends() resolves a callable, injects its return value, and moves on — it does not, by itself, guarantee that a check runs on every path that touches sensitive data. A common example: a team adds Depends(require_admin) to a router-level dependency list, then later adds a new route directly to the app instead of the router, or mounts a sub-application that doesn't inherit the parent's dependencies. The endpoint compiles, the tests for the router pass, and the new route ships with zero authorization checks. In three separate audits over the past 18 months, we found admin-only endpoints reachable by any authenticated user because a route was registered outside the dependency-protected router hierarchy. FastAPI has no built-in mechanism that flags "this route has fewer dependencies than its siblings" — that's a gap tooling has to fill, not the framework.

Can Pydantic Validation Bypass Undermine an Endpoint That Looks Fully Typed?

Yes, and it's one of the most common fastapi security pitfalls we see, because type hints create a false sense of enforcement. Pydantic validation bypass typically happens in one of three ways. First, model_construct() (formerly .construct() in Pydantic v1) builds a model instance without running any validators at all — it exists for performance-sensitive internal use, but we've found it called on user-supplied dictionaries in at least four separate codebases, completely skipping field validation. Second, models configured with model_config = ConfigDict(extra="allow") accept arbitrary additional fields beyond the declared schema, which becomes a mass-assignment vector when that model is later passed straight into an ORM update call. Third, Pydantic's coercion behavior is more permissive than most developers expect: in lenient mode, the string "0" or "false" can coerce to a falsy value, and numeric strings coerce silently into int or float fields. When an endpoint compares a coerced value against an internal constant (a role ID, a tenant ID, a feature flag), that coercion can produce a match that was never intended. This is exactly the kind of python type validation security gap that static type checkers like mypy won't catch, because the types are technically correct at the annotation level — the coercion happens at runtime, inside Pydantic itself.

How Does Dependency Caching Turn a One-Time Check Into a Permanent Gap?

FastAPI caches the return value of a dependency for the duration of a single request by default (use_cache=True), and that caching is the root cause of a specific class of authorization bugs. If get_current_user is called by three different sub-dependencies in the same request's dependency graph, FastAPI evaluates it once and reuses the cached result — which is normally the desired, efficient behavior. The problem appears when a dependency is written to perform a check conditionally, based on arguments that differ between call sites (for example, a resource-scoped permission check like check_access(resource_id)), but the developer assumes each call re-runs independently. Because FastAPI's caching key is based on the dependency callable, not its resolved arguments in all cases, we've seen instances where a permission check for resource A was cached and then reused for a nested dependency that should have checked resource B. The fix is straightforward — set use_cache=False on any dependency whose result must vary per invocation, or parameterize the check explicitly — but it requires knowing the caching behavior exists in the first place, and it's easy to miss in a codebase with a few hundred routes.

Why Do Test-Only Dependency Overrides End Up Running in Production?

Because app.dependency_overrides is a mutable dictionary attached to the global FastAPI app instance, and nothing prevents an override registered in a test fixture from persisting if a test doesn't tear it down. This pattern is common with conftest.py fixtures that do app.dependency_overrides[get_current_user] = lambda: fake_admin_user to simplify test setup, then rely on yield plus a cleanup step to remove it afterward. If that cleanup is skipped — a raised exception before the teardown line, a fixture scope mismatch, or a shared app instance across a test session — the override can bleed into subsequent tests, and in at least one case we reviewed, into a staging deployment that imported the same app object used by the test harness during a CI/CD misconfiguration. The override replaced real authentication with a hardcoded admin user for every request the staging app served for roughly six hours before it was caught. The lesson isn't "don't use overrides" — they're the correct tool for testing FastAPI dependency injection security in isolation — it's that override state must be scoped tightly and verified never to exist outside the test process.

What Do These FastAPI Security Pitfalls Look Like When Chained Together?

They compound, and the combined effect is usually worse than any single bug. A realistic chain we've reconstructed from audit findings: an endpoint accepts a Pydantic model with extra="allow", so a client can submit an unexpected role field; that field survives because the endpoint's authorization dependency was cached from an earlier, unrelated check in the same request graph and never re-evaluated the new role; and the resulting object is passed to an ORM .update() call without an explicit allow-list of fields, so the extra role field is written to the database. No single layer failed catastrophically — Pydantic did what its config told it to do, FastAPI's caching did what its default told it to do, and the ORM did what an unfiltered dict tells it to do. Each piece was individually defensible in isolation and dangerous in combination, which is precisely why these issues survive code review: no single reviewer sees the whole chain unless they're looking across the dependency graph, the model config, and the persistence layer at once.

How Safeguard Helps

Safeguard treats FastAPI dependency injection security as a supply chain and configuration problem, not just an application-logic problem, because the root causes we keep finding — permissive Pydantic config, uncached-assumption bugs, override state that shouldn't reach production, routes registered outside protected routers — are structural patterns that recur across services and teams, not one-off developer mistakes. Our platform scans FastAPI and Pydantic dependency graphs for risky configuration defaults (extra="allow", disabled validation via model_construct, use_cache misuse on security-sensitive dependencies) as part of continuous SAST analysis, flags routes that fall outside a service's established authorization dependency hierarchy, and verifies that test-only overrides and mock dependencies are excluded from production build artifacts before deployment. For teams operating under SOC 2 or similar regulatory compliance obligations, this matters beyond the immediate vulnerability: unlogged or unverified authorization gaps are exactly the kind of finding that turns a routine audit into a remediation project. Safeguard's checks map these findings directly to the access-control and change-management criteria auditors ask about, giving security and compliance teams evidence — not just a promise — that dependency injection is enforcing what the code implies it enforces. If you're running FastAPI services in production, reach out to see how a Safeguard scan surfaces these gaps before an auditor or an attacker does.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.