FastAPI · NoSQL injection

NoSQL injection in FastAPI

FastAPI is the one framework here where the default way of writing an endpoint blocks MongoDB operator injection, because a field declared str cannot arrive as an object, so the bug appears exactly where a model says dict or Any.

Class
NoSQL injection in Python
CWE
CWE-943
web framework
FastAPI documentation
On this page

How it arises in FastAPI

Operator injection needs a JSON object where the code expected a scalar: {"password": {"$ne": null}}. With password: str on a Pydantic model, FastAPI answers that request with a 422 and the handler never runs. This is real protection and it costs nothing.

It disappears in four common situations.

Arbitrary dict bodies. FastAPI documents "bodies of arbitrary dicts": declare weights: dict[int, float] or simply filters: dict and any keys are accepted. Search endpoints use this to let clients send {"status": "open", "owner": "ana"}, then pass the dictionary to find(). The client can equally send {"owner": {"$ne": ""}} or {"$where": "..."}.

Any and untyped parameters. A field typed Any, or a handler that takes payload: dict = Body(...) to stay flexible during prototyping.

Raw request access. await request.json() skips the model layer completely and returns Python objects of whatever shape was sent.

Sort and projection parameters. sort: str split into field and direction and passed to sort(), or a fields list used as a projection. These choose field names. They are not operator injection, but they can expose fields such as password hashes that the response model would otherwise omit when the endpoint returns raw documents.

A note on drivers: Motor's documentation states it will be deprecated on May 14th, 2026 in favour of the PyMongo Async driver, so new FastAPI code is moving to PyMongo's own API.

Vulnerable and fixed

from fastapi import FastAPI
from pymongo import MongoClient

app = FastAPI()
db = MongoClient()["tickets"]


@app.post("/tickets/search")
def search(filters: dict, tenant: str):
    query = {"tenant": tenant, **filters}
    return [{"id": str(t["_id"]), "title": t["title"]} for t in db.tickets.find(query)]

Besides operator injection, a tenant key inside filters overrides the real one. The fix names each searchable field with a scalar type and forbids extras, so an object or an unknown key is a 422:

from enum import Enum
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict
from pymongo import MongoClient

app = FastAPI()
db = MongoClient()["tickets"]


class Status(str, Enum):
    open = "open"
    closed = "closed"


class TicketFilter(BaseModel):
    model_config = ConfigDict(extra="forbid")

    status: Optional[Status] = None
    owner: Optional[str] = None


@app.post("/tickets/search")
def search(filters: TicketFilter, tenant: str):
    query = {"tenant": tenant}
    if filters.status is not None:
        query["status"] = filters.status.value
    if filters.owner is not None:
        query["owner"] = filters.owner
    return [{"id": str(t["_id"]), "title": t["title"]} for t in db.tickets.find(query)]

In a real service tenant comes from the authenticated principal through a dependency, not from a parameter.

What FastAPI and Pydantic already do for you

Scalar field types are the defence, and they are the path of least resistance in FastAPI. Enum fields restrict values to a list. Model configuration can forbid unknown keys. Response models strip fields you did not declare, which limits what a projection mistake can leak. On the database side, MongoDB documents that queries are BSON objects, so string-style injection does not apply, and that server-side JavaScript ($where, $function, $accumulator, mapReduce) can be switched off.

What es-python adds

Type hints protect the fields you typed. es-python reports on the ones that got through. When request data reaches a PyMongo Collection method that takes a filter or pipeline (find, find_one, count_documents, the update_*, delete_* and find_one_and_* methods, aggregate), it records nosql mongodb with the nosql injection impact tag.

The event concentrates on the dangerous parts: $-prefixed operator keys in the filter that arrived from the request, and the values of code-bearing operators such as $where, $expr and $function. A typed string used as a value is ordinary querying. That makes the event a precise signal for this framework: it points at an endpoint where a dict, an Any or a raw request.json() let an operator through, which is the list a reviewer wants.

Coverage here is for PyMongo's Collection methods as described on the class page. If your service uses an async driver, confirm it on your own stack: send one test request with an operator object under es-python with the socket source on and check that the event appears before you rely on it.

The Block backend network attacks template can block nosql mongodb: the query call raises a RuntimeError before it runs when an operator came from the request.

Review checklist

  1. Search models and handlers for dict, Any, Body(...) without a model, and request.json().
  2. Replace free-form filters with a model of typed, optional fields and extra="forbid".
  3. Take the tenant from authentication, and add it to the query last.
  4. Map sort and projection choices through an Enum.
  5. Disable server-side JavaScript in MongoDB if no feature needs it.

References

See it on your own FastAPI code

es-python runs your application unchanged and reports when request data reaches the operation, with where the data came from and the line that used it. A live demo walks through it on a real application.

EyalSec Pricing Docs Security Research Vulnerabilities Security glossary About Contact עברית Login Book a live demo