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
- Search models and handlers for
dict,Any,Body(...)without a model, andrequest.json(). - Replace free-form filters with a model of typed, optional fields and
extra="forbid". - Take the tenant from authentication, and add it to the query last.
- Map sort and projection choices through an
Enum. - Disable server-side JavaScript in MongoDB if no feature needs it.
References
- Reference: FastAPI documentation: Body, Nested Models, bodies of arbitrary dicts (checked 2026-10-12)
- Reference: MongoDB documentation: FAQ, How does MongoDB address SQL or Query injection? (checked 2026-10-12)
- Reference: Motor documentation: Tutorial, deprecation notice (checked 2026-10-12)
- Reference: Pydantic documentation: Configuration (checked 2026-10-12)