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.
{"$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,$gtor$regex, logging in as any account without a credential. - Extract data character by character using
$regexanchored 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
$wherestring-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$wherecannot 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
$wherealone: that removes one evaluator, not operator injection into ordinary filters.
A developer review checklist
- Define request schemas with exact types, lengths and allowed keys.
- Construct filter and pipeline objects explicitly on the server.
- Search for spreads, merges or direct
req.bodyuse inside database calls. - Keep authentication password verification outside flexible database filters.
- Disable server-side scripting when the deployment does not need it.
- 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.