NoSQL Operator Injection: When a Scalar Field Becomes a Filter
When a parser turns a scalar field into an object, keys such as $ne, $gt and $regex can become MongoDB query logic. Learn both request shapes, a safe proof, and strict schema defenses.
sku[$ne]=At a glance
CWE-943- Interpreter
- Document query engine
- Required condition
- A scalar request field can arrive as an object containing query operators.
- Potential impact
- Filters can be widened or changed, exposing records or bypassing application checks.
- Primary defense
- Reject non-scalar types and build filters from an explicit server-owned schema.
Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.
On this page
The most common form of NoSQL injection is not exotic code execution — it is a query operator slipped into a field the application expected to hold a plain string. MongoDB filters are themselves documents, so the difference between a value and a piece of logic is only the type of what you pass. When a request parser hands the driver an object where the code assumed a scalar, attacker-supplied keys like $ne, $gt, $regex and $in become query operators, and the filter stops meaning what the developer wrote.
This post walks through the two request shapes — raw JSON and URL-encoded bracket notation — and shows how each can turn an exact lookup into a wider filter when parser and driver behavior align.
When this technique applies
Operator injection needs a request field that can arrive as an object, application code that does not reject the wrong type, and a driver call that places that object in query structure. A framework that parses bracket notation as a literal key will behave differently from one configured to create nested objects. JSON always preserves the object type, but a schema validator can stop it before query construction.
How it happens
The root cause is that body and query parsers build rich objects, and the application passes a value straight into a filter without checking its type. Consider a product lookup:
const { sku } = req.body; const product = await db.collection('products').findOne({ sku }); res.json(product);
For a normal request sku is a string and the filter is an equality check. If it arrives as an object, that object can be interpreted as query structure and the exact lookup is gone.
A safe proof: widening an exact lookup
Instead of a string, an authorized tester sends an operator against seeded lab products:
{ "sku": { "$ne": null } }
The driver executes:
db.collection('products').findOne({ sku: { $ne: null } })
$ne means “not equal,” so the database can return the first product whose SKU is not null instead of an exact requested SKU. Use marker-only lab records and compare this with an impossible exact SKU. The equivalent bracket form is relevant only if the deployed body parser expands it into a nested object:
POST /product HTTP/1.1
Content-Type: application/x-www-form-urlencoded
sku[$ne]=
Inspect the parsed server value during lab development: some configurations produce { sku: { $ne: '' } }, while others produce a literal key named sku[$ne]. The latter does not demonstrate operator injection.
The decisive change is not a magic substring. It is the field's runtime type: scalar data became a query object.
What an attacker gains
Once a field can carry operators, the effect depends on where that field is used:
- Widen an exact lookup with comparison or existence operators.
- Change a search predicate with
$in,$ninor$regexwhen those objects reach a filter. - Create a blind decision channel when the endpoint's success state reveals whether an injected predicate matched.
- Increase query cost with expensive regular expressions or unexpectedly broad scans.
These outcomes are bounded by the surrounding filter, projection, authorization checks and database privileges. An operator inside sku cannot automatically replace an unrelated tenant restriction unless application code also lets request data control that structure.
The fix: reject objects where you expect strings
The reliable defence is to stop request-supplied data from changing a query's structure. Validate the type of every input, and pin equality explicitly so an operator object can never be interpreted as logic.
Reject the wrong type early so a string field can only ever be a string:
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: { $eq: req.body.sku } });
Now an object is rejected before the driver call. The explicit $eq makes the intended comparison visible in code, but it is not a replacement for type validation. Layer these on top:
- Use request schema validation with rejection, not silent object-to-string coercion.
- Validate request bodies against an explicit schema and reject any payload whose keys begin with
$, in both the JSON and bracket-expanded forms. - Configure the body parser to disallow nested objects in fields that are declared as scalars, so
sku[$ne]=cannot become an object at all.
Safe verification and false signals
- Use only seeded records with obvious marker values.
- Capture the parsed request type and the final query shape in a local lab.
- Pair an operator-shaped request with an impossible exact lookup.
- Do not infer real hashes, tokens or personal fields through regex probes.
- Do not mistake normal flexible-search features for injection when the server intentionally maps allowed operators.
How SelfSec finds it
SelfSec crawls the target and probes JSON bodies, form fields and query parameters with database-specific operator shapes, exercising both raw JSON such as {"$ne":null} and URL-encoded bracket forms that some parsers expand. It requires repeatable response differences or database-specific error evidence before reporting a finding. Every result includes a reproduction request. Core scan processing runs locally on 127.0.0.1.
References
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.