Skip to main content

NoSQL Injection (MongoDB operator injection)

A route reads a request field and drops it, unparsed, into a MongoDB query filter as a field value. A plain string behaves; a query-operator object such as {"$ne": null} changes the shape of the filter, which is an authentication bypass or a data-exfiltration primitive.

What Vulkro detects

Three pattern-level rules for JavaScript / TypeScript and Python, all High severity. There is no cross-function taint here: the detector reads the call site.

NOSQLI-001 fires when a MongoDB read or write sink (find, findOne, updateOne, deleteOne, aggregate and friends, or the PyMongo equivalents) binds a request value (req.body.X, req.query.X, req.params.X, Python request.json[...], request.form[...], request.args.get(...)) directly to a non-operator field key with no string or typed coercion.

NOSQLI-002 fires on a $where clause built from a template literal or string concatenation carrying user input. $where evaluates its value as server-side JavaScript, so that is a code-execution sink. A static $where string literal is not flagged.

NOSQLI-003 fires when the entire request object is passed as the filter (Model.find(req.body), coll.find(request.json)), which makes every field the attacker sends a query operand.

Deliberately not flagged, to keep the signal clean: a value coerced to a string or typed value (String(...), Number(...), new ObjectId(...), .toString(), Python str(...) / int(...)); a user value bound under an explicit operator key the code controls, such as { age: { $gt: req.query.age } }; and a static $where literal. Files with no MongoDB context are skipped entirely, so a plain Array .find(callback) or a SQL ORM never trips the detector.

Remediation. Coerce every user-supplied filter value to the type the field expects before it reaches the driver (String(...), new ObjectId(...), a schema validator such as zod or pydantic), and never pass a request body straight in as a filter. Replace $where with a real query operator or an aggregation expression.

Non-compliant code (examples)

Express + Mongoose - request value bound straight into the filter

app.post('/login', async (req, res) => {
const user = await User.findOne({
username: req.body.username,
password: req.body.password, // {"$ne": null} matches any row
});
res.json(user);
});

Compliant code (examples)

Express + Mongoose - coerced to strings, password verified out of band

app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '');
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(String(req.body.password ?? ''), user.hash))) {
return res.sendStatus(401);
}
res.json({ id: user.id });
});

See also

  • Confidence model - what High, Medium, and Low mean for findings in this category.
  • Safety - what Vulkro does and does not access on your machine.

References


This page is generated by vulkro rules export <out-dir> from Vulkro's built-in detector catalogue. Edits made by hand are overwritten on the next regeneration.