How it arises in Flask
MongoDB's own documentation makes the reassuring half of the point: a client program assembles a query as a BSON object, not a string, so traditional SQL injection attacks are not a problem. A value like ' OR 1=1 is just a string that matches nothing.
The other half is about types. In PyMongo a filter is a Python dictionary, and a dictionary value can itself be a dictionary of operators. Flask does not fix the type of a JSON field.
Operator injection. The view expects {"username": "ana", "password": "secret"} and builds {"username": u, "password": p}. The client sends {"username": "admin", "password": {"$ne": null}}. The filter becomes "username is admin and password is not null", and find_one() returns the admin document.
Form and query-string variants. request.args and request.form hold strings, which is safer by accident. Code that parses a filter parameter with json.loads() to support "advanced search" reintroduces dictionaries.
Whole-filter passthrough. collection.find(request.get_json()) for an internal dashboard. The caller chooses every field and operator, including ones that read other tenants' documents.
$where and server-side JavaScript. MongoDB lists $where, mapReduce, $accumulator and $function as operations that run JavaScript expressions on the server and says you must exercise care to prevent users from submitting malicious JavaScript. A $where string built with an f-string is code injection into the database.
Vulnerable and fixed
from flask import Flask, jsonify, request
from pymongo import MongoClient
app = Flask(__name__)
db = MongoClient()["shop"]
@app.post("/login")
def login():
body = request.get_json()
user = db.users.find_one({"username": body["username"], "password": body["password"]})
return jsonify(ok=user is not None)
@app.get("/products")
def products():
limit = request.args.get("max_price", "100")
rows = db.products.find({"$where": f"this.price < {limit}"})
return jsonify([r["name"] for r in rows])
Require the types you expect before building the filter, and express the comparison with an operator and a number, not JavaScript. (Passwords belong in a hash compared in Python; that change is separate from the injection fix and shown here.)
from flask import Flask, abort, jsonify, request
from pymongo import MongoClient
from werkzeug.security import check_password_hash
app = Flask(__name__)
db = MongoClient()["shop"]
@app.post("/login")
def login():
body = request.get_json(silent=True) or {}
username, password = body.get("username"), body.get("password")
if not isinstance(username, str) or not isinstance(password, str):
abort(400)
user = db.users.find_one({"username": username})
ok = user is not None and check_password_hash(user["password_hash"], password)
return jsonify(ok=ok)
@app.get("/products")
def products():
limit = request.args.get("max_price", default=100.0, type=float)
rows = db.products.find({"price": {"$lt": limit}})
return jsonify([r["name"] for r in rows])
What Flask, PyMongo and MongoDB already do for you
PyMongo sends filters as BSON, so there is no query string to break out of. Werkzeug's request.args.get(..., type=float) converts and falls back to the default on bad input, which is the shortest type check available. MongoDB can disable server-side JavaScript entirely with security.javascriptEnabled: false or --noscripting, and its documentation notes that most operations can be expressed without JavaScript.
What es-python adds
es-python reports when request data reaches a PyMongo Collection method that takes a filter or a pipeline: find, find_one, count_documents, the delete_*, update_* and find_one_and_* methods, and aggregate. The label is nosql mongodb, with the nosql injection impact tag.
The report is narrowed to what makes this class dangerous. A request string used as a plain value, as in the fixed login, is ordinary querying. What es-python singles out are $-prefixed operator keys in the filter that came from the request, and the value of the code-bearing operators such as $where, $expr and $function. So the {"$ne": null} login and the formatted $where string are both reported, and the event shows the operator and the view.
That gives you a direct test. Replay a login with an operator object as the password under es-python with the socket source on. If the event appears, the type check is missing somewhere on that path. An event built from network data is graded Critical.
The ready-made Block backend network attacks template includes nosql mongodb: with it applied, a MongoDB query operator that came from the request makes the call raise a RuntimeError before the query runs, and correct queries are not blocked.
Review checklist
- Search for
find(,find_one(,update_one(anddelete_many(whose filter contains a value fromrequest. - Check
isinstance(value, str)(or convert) before a value enters a filter. - Never pass a client-built dictionary as the filter.
- Replace
$wherewith query operators; disable server-side JavaScript if you can. - Validate bodies with a schema library so types are enforced in one place.
References
- Reference: MongoDB documentation: FAQ, How does MongoDB address SQL or Query injection? (checked 2026-10-12)
- Reference: MongoDB documentation: $where (checked 2026-10-12)
- Reference: Werkzeug documentation: Data Structures, get() with type (checked 2026-10-12)
- Reference: PyMongo documentation: Collection (checked 2026-10-12)