MongoDB JavaScript Injection: Why $where Must Stay Server-Owned
MongoDB can evaluate JavaScript in legacy query features such as $where. Learn how string construction crosses into code, how to prove it harmlessly, and why MongoDB 8.0 deprecation strengthens the case for removal.
0||6*7===42At a glance
CWE-95- Interpreter
- Database-hosted JavaScript engine
- Required condition
- Attacker data reaches a server-side JavaScript expression such as MongoDB $where.
- Potential impact
- Authorization bypass, blind inference, or denial of service inside the database.
- Primary defense
- Remove dynamic JavaScript from queries and disable server-side scripting when unused.
Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.
On this page
Operator injection bends a MongoDB query's logic, but it stays inside the document query language. Server-side JavaScript injection crosses into a JavaScript evaluator. MongoDB's $where, $function and $accumulator can evaluate JavaScript, so concatenating request data into their source turns a query value into code inside the database process.
MongoDB deprecated server-side JavaScript functions starting in version 8.0, and deprecated map-reduce starting in 5.0. Deprecation is not automatic removal: existing deployments may still execute these features. This post focuses on $where, where the trust boundary is easiest to see.
When this technique applies
The application must construct JavaScript source from untrusted data and pass it to a server-side JavaScript feature. Merely allowing ordinary MongoDB operators is a different issue. Server-side scripting must also be enabled, and the relevant function must exist in the deployed MongoDB version.
The JavaScript environment is database-specific and is not the same as Node.js. It does not automatically expose require, the host filesystem or the application's runtime objects. Impact comes from query control, access to the current document, resource consumption and whatever functions the database intentionally exposes.
How it happens
$where lets a developer express a match as a JavaScript predicate evaluated against each document, with this bound to the current record. The trouble starts when the predicate is built by string concatenation. Consider a search that filters by a user-supplied minimum balance:
const min = req.query.min; const accounts = await db.collection('accounts').find({ $where: `this.balance > ${min}` }).toArray();
For min=100 the database evaluates this.balance > 100 per document. But min is concatenated raw into a string that the engine will execute, so the attacker does not have to supply a number — they can supply code, and the database will run whatever they write.
A safe proof: a fixed expression changes the predicate
In a seeded lab, the tester appends a fixed arithmetic expression:
GET /accounts?min=0||6*7===42 HTTP/1.1
The evaluated predicate becomes this.balance > 0||6*7===42, so it returns the seeded lab documents even when their balance is not greater than zero. Pair it with 0||6*7===41, which should preserve the original filter. A stable difference demonstrates that the request changed evaluated JavaScript without accessing a private field or causing a delay.
The attacker did not match a document — they supplied a program.
$wherehanded user input to a JavaScript engine running inside the database, and the engine did exactly what the input said.
What the boundary exposes
JavaScript source control inside the database can expose several classes of impact:
- Predicate manipulation that widens or changes which documents match.
- Blind inference when a response or timing difference reflects a condition evaluated against document fields.
- Resource exhaustion through expensive computation evaluated for many documents.
- Unexpected field access through
this, even if the endpoint's final projection hides those fields.
Projection, authorization checks and database privileges still matter. A true predicate does not automatically serialize every field, and database JavaScript is not automatically operating-system RCE. The vulnerability remains serious because attacker data is running as program logic in a privileged query path.
The fix: take JavaScript out of the query path entirely
$where exists for cases ordinary operators cannot express, and it is almost never one of them. The fix is to stop evaluating user input as code and to remove the engine's ability to run it at all.
First, replace the $where string with ordinary, typed query operators that compare values rather than execute code:
const min = Number(req.query.min); if (!Number.isFinite(min)) { return res.status(400).send('Invalid input'); } const accounts = await db.collection('accounts').find({ balance: { $gt: min } }).toArray();
The value is now a number the driver compares, never a string the database runs. Layer these on top:
- Disable server-side JavaScript in MongoDB (
--noscripting/security.javascriptEnabled: false), so$where,$function,$accumulatorand map-reduce cannot execute code even if a sink is missed. - Never build a
$where,$function,$accumulatoror map-reduce body from user input — treat them as code, not data. - Express every filter with parameterized query operators (
$gt,$eq,$in), which the driver evaluates without an interpreter.
MongoDB's group command was removed before the current server-side JavaScript deprecation and should not be presented as a modern alternative. Prefer aggregation pipeline stages and native query operators that do not execute custom JavaScript.
Safe verification and false signals
- Confirm the deployed MongoDB version and whether scripting is enabled.
- Use fixed arithmetic against seeded lab documents, with true and false controls.
- Do not use unbounded loops, sleeps, large allocations or private document fields.
- Distinguish application-side JavaScript evaluation from database-side
$whereexecution. - Treat a parser or WAF error as evidence of blocking, not proof that JavaScript ran.
How SelfSec finds it
SelfSec crawls the target and probes JSON bodies, form fields and query parameters for server-side expression behavior, including JavaScript-capable MongoDB query surfaces. It pairs always-true and always-false predicates and, where supported, controlled timing probes, then requires repeatable response or timing 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.