Chapter 2 · Practice 3 of 5
Stop operator injection in document databases
Why it matters
NoSQL databases do not parse SQL, but they have their own injection. In MongoDB, a filter is an object, and an object can contain operators. If a login handler passes req.body.password straight into a query and an attacker sends {"$ne": null}, the filter matches any password.
Defenses:
- Validate types at the boundary. If the field is a string, the schema rejects an object. This one check stops most operator injection.
- Build filters from scalars you extracted, never from a request object you spread in.
- Avoid server-side JavaScript such as
$whereand$function, which evaluate code inside the database. - Use your driver's or ODM's sanitizing option (for example Mongoose's
sanitizeFilter) as a second layer, not the only one.
The same pattern hits any API that turns JSON into a query: Elasticsearch query DSL, GraphQL filter inputs, ORM where objects built from request bodies.
Example
// Vulnerable: { "email": "a@b.c", "password": { "$ne": null } } matches
await users.findOne({ email: req.body.email, password: req.body.password });
// Safe: the schema rejects anything that is not a string
const { email, password } = z
.object({ email: z.string().email(), password: z.string().max(256) })
.parse(req.body);
const user = await users.findOne({ email: email.toLowerCase() });
// then verify the password hash in code, never in the filter