Your MongoDB login looks clean. There's no string concatenation anywhere, so surely there's nothing to inject:
const user = await User.findOne({
username: req.body.username,
password: req.body.password,
});
if (user) grantSession(user);Then an attacker sends this JSON body:
{ "username": "admin", "password": { "$gt": "" } }req.body.password is no longer a string — it's the object { "$gt": "" }, a MongoDB query operator meaning "greater than empty string," which every stored password satisfies. The query becomes "find the admin user whose password is anything," and the attacker is logged in as admin without knowing the password. No quotes, no OR 1=1, no SQL at all — but the same root cause: untrusted data changed the shape of the query.
The two flavors of NoSQL injection
Operator injection. Because MongoDB queries are just objects, and JSON request bodies deserialize into objects, an attacker who controls a value can substitute an operator object for the plain value you expected. The workhorses:
{"$ne": null}— "not equal to null," matches almost everything{"$gt": ""}— "greater than empty," matches every non-empty value{"$regex": "^adm"}— pattern matching, useful for extracting data character by character{"$in": [...]}— match against an attacker-supplied list
JavaScript injection via $where. Some queries accept a $where clause containing JavaScript that runs on the database server. If user input reaches it, the attacker isn't just bypassing a filter — they're running code:
// Dangerous: user input inside server-side JS
db.users.find({ $where: `this.username === '${input}'` });
// input = "'; return true; //" -> matches every document
// input = "'; while(true){} //" -> denial of serviceBlind operator injection with $regex also enables data exfiltration: by asking "does the admin's password start with a? with b?" and watching which requests succeed, an attacker can extract secrets one character at a time — the NoSQL version of a blind SQL injection.
Where it comes from
Almost every NoSQL injection traces back to one habit: passing a request body or query string straight into a database filter without checking its type. Common entry points:
- Login and lookup endpoints that spread
req.bodyinto afindfilter - Search features that forward query parameters into
$regexor$where - APIs that accept a JSON "filter" object from the client and run it directly
- Query-string parsers (like
qs) that turn?password[$ne]=into a nested object for you — turning even a GET request into operator injection
The fix: make values be values
The defense is to guarantee that a field you expect to be a string can never arrive as an operator object. There are three complementary ways to enforce that.
The principles behind the code
Strip the language away and NoSQL injection defense comes down to three rules:
- Type every input. A field you treat as a string must be a string by the time it reaches the query. Validate with a schema (Zod, Joi, Pydantic, a Mongoose schema with strict types) so an object can never masquerade as a value.
- Never let user input choose operators or run code. Build filters yourself. Don't accept a raw filter object from the client, and disable
$where/mapReduceserver-side scripting entirely. - Don't compare secrets in the query. Look records up by a public identifier, then verify passwords and tokens in application code with a constant-time hash comparison. A query should never contain a raw secret to match against.
The one-line version: in MongoDB, an object where you expected a string is an attack. Guarantee the type and the whole class of bug disappears.
See it live in the NoSQL injection simulation — log in as admin with a $gt payload, then add schema validation and watch it fail. For the relational cousin, read the SQL injection prevention guide; the databases differ but the lesson is identical.
Frequently Asked Questions
Related posts
Server-Side Template Injection: From {{7*7}} to RCE
SSTI happens when user input is rendered as part of a server-side template instead of passed as data. Here's how {{7*7}} becomes remote code execution, and how to render templates safely in Jinja2, Twig, and Node.js.
Aug 8, 2026 · 7 min readLDAP Injection: Bypassing Auth Through the Directory
LDAP injection lets an attacker rewrite a directory query with characters like * ( ) and | — bypassing login and dumping user data. Here's how the filter syntax gets abused and how to escape it correctly in Node.js, Python, and Java.
Aug 7, 2026 · 7 min readPath Traversal: Reading Files You Were Never Meant to Reach
Path traversal (directory traversal) turns a file parameter into a key to your whole filesystem. Here's how ../ attacks work, why string filtering fails, and how to canonicalize and confine paths in Node.js, Python, Java, and PHP.
Aug 5, 2026 · 8 min read