NoSQL Injection

NoSQL Injection: When JSON Operators Rewrite a Query

Switching from SQL to a document database does not remove injection — it changes the syntax. Learn how attacker-controlled types and operators rewrite filters, and how explicit schemas keep query structure on the server.

SelfSec Team6 min read
Part 1 of 3from theNoSQL Injectionseries
{"$ne": null}

At a glance

CWE-943
Interpreter
Document or non-relational query engine
Required condition
Request-controlled objects or operators are passed into query structure.
Potential impact
Filter bypass, excess data access, query abuse, or server-side evaluation.
Primary defense
Validate types and schemas, then construct queries from trusted operators only.

Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.

On this page

A common myth is that NoSQL databases are immune to injection because they do not speak SQL. They are not. NoSQL injection happens whenever untrusted input is passed into a query language as logic instead of as a plain value. In MongoDB that usually means smuggling query operators such as $ne, $gt or $regex into a request the application expected to contain a string. The result can be a widened filter, unauthorized records, blind inference or expensive query behavior.

This post focuses on MongoDB, where the object-versus-value distinction is easy to see. Other database families have different query languages and defenses: Cassandra CQL uses bound parameters, CouchDB has Mango selectors, and Redis uses structured command arguments. “NoSQL” is an umbrella, not one wire format.

The mental model: the attacker controls type as well as text

In a form field, developers tend to picture input as a string. JSON can carry strings, numbers, arrays and objects. A document database query is also an object, so accepting an object where the application expected a string can move attacker data directly into the query's structure.

Request value Application assumption Query meaning
"published" Scalar status Status equals published
{ "$ne": "draft" } Still “a status” Status is anything except draft
{ "$regex": "^a" } Still “a search term” Apply a regular-expression operator

The safe boundary is not “valid JSON.” It is an application schema that decides the exact type and permitted meaning of each field before the database driver sees it.

How it happens

The root cause is that web frameworks parse request bodies and query strings into rich objects, and the application then hands those values straight to the driver. Consider a product lookup that expects one SKU:

const { sku } = req.body;

const product = await db.collection('products').findOne({ sku });
res.json(product);

For a normal lookup, sku is a string and the query means exactly what it looks like. But the value comes from the request body, and the attacker controls its type, not just its characters. If they can make sku an object, they can supply a MongoDB operator.

A concrete attack: widening a lookup

Instead of a JSON body with string fields, the attacker sends:

{ "sku": { "$ne": null } }

The query the driver actually executes becomes:

db.collection('products').findOne({
  sku: { $ne: null }
})

$ne means “not equal.” The filter now asks for the first product whose SKU is not null instead of the one product named by the caller. If the endpoint returns fields intended only for exact internal lookups, the type confusion has crossed an authorization boundary. The same shape can appear in URL-encoded bodies when a parser expands bracket syntax into nested objects:

POST /product HTTP/1.1
Content-Type: application/x-www-form-urlencoded

sku[$ne]=

That request no longer performs an exact SKU lookup. Whether a framework accepts this bracket form depends on its parser configuration, which is why testing the actual parsed type matters more than memorizing one encoding.

The attacker did not break JSON or MongoDB. They supplied a valid object where the application promised itself there would be a string.

Authentication bypass examples often show a query comparing a plaintext password field to { "$ne": null }. That demonstrates operator behavior but also relies on an already broken password design. A sound login validates scalar input, fetches one account by a normalized identifier, and verifies the stored password hash in application code. It never asks the database to compare a request password as part of a flexible document filter.

What an attacker gains

Operator injection is rarely the end of it. Once input is treated as query logic, the attacker can:

  • Bypass authentication with $ne, $gt or $regex, logging in as any account without a credential.
  • Extract data character by character using $regex anchored probes ({"$regex":"^a"}), peeling out password hashes and tokens through blind, response-shape feedback.
  • Dump whole collections by forcing always-true filters such as {"$gt":""} or {"$exists":true} on a listing endpoint.
  • Abuse server-side JavaScript where a separate $where string-concatenation sink exists, creating logic and denial-of-service risks inside the database engine.
  • Inject aggregation stages only when an application accepts request-controlled pipeline structure, a more severe version of the same trust-boundary failure.

MongoDB deprecated server-side JavaScript functions including $where, $function and $accumulator starting in MongoDB 8.0. Existing deployments can still have them enabled, so removing dynamic JavaScript and disabling scripting when unused remain important hardening steps.

The fix: reject objects where you expect strings

The reliable defence is to never let request-supplied data change the structure of a query. Two rules cover most of it: validate the type of every input, and use typed query methods rather than passing raw objects through.

First, enforce that a field that should be a string really is one. Reject the wrong type before constructing the query; do not coerce an object with String(), because coercion can hide malformed requests and create surprising values:

if (typeof req.body.sku !== 'string' || req.body.sku.length > 64) {
  return res.status(400).send('Invalid input');
}

const product = await db.collection('products').findOne({
  sku: req.body.sku
});

Now { "$ne": null } is rejected rather than reaching the driver. The query object and its only operator, equality, remain owned by the application. Layer these on top:

  • Validate each request body against an explicit schema and reject unknown keys and wrong types.
  • Disable server-side JavaScript on MongoDB (--noscripting / security.javascriptEnabled: false) so $where cannot execute code even if input leaks through.
  • Reject request-controlled keys that begin with $ or contain . as defense in depth, but do not use that check instead of a schema.
  • For Cassandra, use prepared statements with bound parameters; for Redis, never concatenate user input into commands.

Fixes that do not hold

  • Checking only for $ne: MongoDB exposes many operators, and other engines use different syntax.
  • Converting every value to a string: coercion can silently accept malformed objects; explicit rejection makes the contract enforceable.
  • Relying on ODM defaults: casting and strictness behavior varies by operation and version, especially for raw filters and aggregation pipelines.
  • Validating JSON syntax: syntactically valid JSON can still carry dangerous query structure.
  • Disabling $where alone: that removes one evaluator, not operator injection into ordinary filters.

A developer review checklist

  1. Define request schemas with exact types, lengths and allowed keys.
  2. Construct filter and pipeline objects explicitly on the server.
  3. Search for spreads, merges or direct req.body use inside database calls.
  4. Keep authentication password verification outside flexible database filters.
  5. Disable server-side scripting when the deployment does not need it.
  6. Apply collection-level privileges and query resource limits to reduce impact.

References

How SelfSec proves this one

Scanner behaviour
Module
NoSQL Injection
How it probes
Operator injection and type-confusion payloads in JSON bodies and query parameters against document stores.
How it confirms
Boolean-differential responses measured against a per-target timing baseline separate a real operator injection from an application error.

Reported as CWE-943 · ATT&CK T1190 · SARIF 2.1.0

A reflected response alone is not enough to promote a NoSQL Injection finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about NoSQL Injection

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The NoSQL Injection series

  1. 01OverviewNoSQL Injection: When JSON Operators Rewrite a QueryReading
  2. 02Operator InjectionNoSQL Operator Injection: When a Scalar Field Becomes a Filter4 min
  3. 03Server-Side JavaScriptMongoDB JavaScript Injection: Why $where Must Stay Server-Owned4 min

Related reading