How it shows up in Python
MongoDB does not parse a string query, so classic SQL-style injection does not apply, but a different door is open. A filter is a dictionary, and if a request value is dropped into it unchanged, the attacker can replace a plain value with an operator document:
from flask import request
def find_user(db, body):
return db.users.find_one({"username": body["username"], "password": body["password"]})
If the request body is JSON and the client sends {"$ne": null} where a password string was expected, the filter becomes "password not equal to null", which matches a record regardless of the real password. The fix is to force each value to the type you expect before it reaches the query, so an operator document can never take a value's place:
from flask import request, abort
def find_user(db, body):
username = body.get("username")
password = body.get("password")
if not isinstance(username, str) or not isinstance(password, str):
abort(400)
return db.users.find_one({"username": username, "password": password})
A schema library such as Pydantic does the same at the edge and gives you typed fields everywhere after. Two deeper cases need care: a $where clause or a mapReduce runs a JavaScript expression inside the database, so a tainted value there is code injection, not just operator injection, so keep user input out of it; and Redis is safe for keys and values (its protocol frames them by length) but EVAL runs a Lua script, so a script body built from user input is injection into that script.
Why it is hard to see
- The payload is structure, not characters.
{"$ne": null}contains no SQL metacharacters and no shell syntax, so signature rules written for those find nothing to flag. - It arrives as ordinary JSON. The attack is a well-formed JSON object; a web application firewall sees a valid body.
- The operator is added in the application. The request sends a value and the code wraps it in a filter dict, so the dangerous shape only exists at the query call.
- No request at all. A worker builds a MongoDB filter from a queue message or a stored document.
What the es-python event looks like
es-python reports when untrusted data reaches a query call on a supported driver. For PyMongo it covers the Collection methods that take a filter or a pipeline (find, find_one, count_documents, the delete_*, update_* and find_one_and_* methods, and aggregate), with the label nosql mongodb. For Redis it covers eval and eval_ro, the Lua entry points, with the label nosql redis. Both carry the nosql injection impact tag. For MongoDB the report narrows to the parts that matter: the $-prefixed operator keys in the filter, and the value of the code-bearing operators such as $where, $expr and $function.
Redis is in the list only for EVAL, and that is deliberate: a tainted key or value cannot escape its length-prefixed RESP field, so storing user data in Redis is the normal case and does not fire. A tainted Lua script body is genuine code injection into the database, and that is what the nosql redis label marks. With network data the event is graded high. Under a Raise rule, the query raises a RuntimeError before it runs.
How to fix and prevent it
- Coerce every request value to its expected type before it reaches a query, so an operator document cannot stand in for a value. Validate with a schema library at the edge.
- Keep user input out of every
$where,$expr,$functionormapReducebody and Redis Lua script. - Keep operator keys out of user-controlled positions: a filter's keys should come from your code, not from the request.
- Give the database account the least privilege it needs.
- In Report mode, watch the
nosql injectiontag to see which queries receive request data directly.
Classification
NoSQL injection is CWE-943, Improper Neutralization of Special Elements in Data Query Logic, a child of CWE-74, Injection. CWE-943 is not individually listed in the OWASP 2025 mapping, but its parent CWE-74 sits under A05:2025 Injection, which is where this class belongs.
Questions people ask
The questions that come up on forums and in reviews about this class, each answered in a few sentences.
Is MongoDB vulnerable to injection from Python?
Yes, in a different shape from SQL: the risk is not a string that becomes query text but a value that becomes a query operator, such as a request parameter that arrives as a dictionary with a $ne key instead of a string and matches every document. PyMongo passes a dictionary through as written, so the fix is to check the type of each value before it goes into a filter and to reject keys that start with $. $where and the other code-bearing operators take JavaScript and deserve the same care as eval().
How do I prevent NoSQL injection in PyMongo?
Validate the type of every value that reaches a filter, so a string field cannot arrive as a dictionary of operators, and build filters from fields you name rather than from a request body passed through whole. Avoid $where, $expr with user input and $function, which evaluate code on the server. es-python reports the $-prefixed operator keys and the code-bearing operator values that came from untrusted data, so the shapes that matter are the ones you see.
How does es-python report NoSQL injection?
It reports when untrusted data reaches a PyMongo query method, find, update_one, aggregate and the rest, or a Redis EVAL, with the label nosql mongodb or nosql redis and the nosql injection impact tag. For MongoDB the report narrows to the operator keys and the code-bearing operator values, because a tainted plain value is the normal case. With network data the event is graded High, and a Raise rule stops the query before it runs.