Coreframe Labs

Automated security audit

Security audits for FastAPI

FastAPI's dependency injection makes authorization declarative, which is a real improvement — right up until a dependency is declared in a way that never executes. That, plus response models that serialize more than intended, covers most of what we find.

Check a file now — free

Paste one file. The free tier runs a single security pass and returns up to three findings, each with the line of code that evidences it. Nothing is stored, nothing is used for training, and detected secrets are redacted before analysis.

Paste one file0 / 400 lines

Not stored. Not trained on. Secrets redacted before analysis.

What we find most often in FastAPI

Each of these is a real pattern, with the code that causes it and the change that fixes it.

1. A security dependency that never runs

Symptom
The endpoint has an auth dependency listed and is completely open.

The pattern

@app.get("/admin/users")
async def list_users(current_user=Depends(get_current_user)):
    # get_current_user returns None for anonymous callers instead of raising
    return await db.fetch_all("SELECT * FROM users")

Why it bites
The dependency resolves successfully with a `None` user and the handler never checks it. FastAPI enforces nothing about the value — a dependency that returns rather than raises is documentation, not a gate.

The fix

async def require_admin(user=Depends(get_current_user)):
    if user is None:
        raise HTTPException(401, "Not authenticated")
    if not user.is_admin:
        raise HTTPException(403, "Forbidden")
    return user

@app.get("/admin/users", dependencies=[Depends(require_admin)])
async def list_users():
    ...

2. Returning the ORM object directly

Symptom
The API response contains `hashed_password` and internal flags.

The pattern

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return await User.get(user_id)   # no response_model

Why it bites
Without a `response_model`, FastAPI serializes every attribute on the returned object. Any column added to the model later — a password hash, an internal note, a reset token — is published automatically the moment it exists.

The fix

class UserOut(BaseModel):
    id: int
    email: EmailStr
    display_name: str
    model_config = ConfigDict(from_attributes=True)

@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
    return await User.get(user_id)   # filtered to the declared fields

3. Blocking I/O inside an async endpoint

Symptom
Throughput collapses under concurrency even though the server is not CPU-bound.

The pattern

@app.get("/report")
async def report():
    data = requests.get("https://slow-api.example.com/data").json()  # blocking
    return data

Why it bites
`requests` blocks the thread, and in an `async def` handler that thread is the event loop. One slow upstream call stalls every concurrent request on that worker — it presents as a capacity problem rather than a code problem.

The fix

@app.get("/report")
async def report():
    async with httpx.AsyncClient(timeout=5.0) as client:
        response = await client.get("https://slow-api.example.com/data")
    return response.json()

# If a sync library is unavoidable, declare the handler with plain "def" —
# FastAPI then runs it in a threadpool instead of on the loop.

An automated pass reads one file. An audit reads the system.

The tool above is deliberately narrow — one file, one dimension, one pass. The issues that actually take systems down live between files: in the trust boundary nobody documented, the query pattern that only emerges at scale, the cache that was correct until the second tenant arrived. That is what the Coreframe Technical Audit is for — five business days, a written report, a live findings call, and a remediation proposal you can execute with or without us.

£1,500–£2,500 solo · £3,000–£5,000 funded startup · £3,000–£6,000 SME

See what the full audit covers →

Related audits

All stack audits →