Secure Coding Best Practices

Chapter 2 · Practice 3 of 5

Stop operator injection in document databases

From Injection: keep data out of commands

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 $where and $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

TypeScript
// 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