NoSQL Injection: The Complete Practical Guide to MongoDB Injection

MongoDB $where and Server-Side JavaScript Injection

How injecting into a $where clause runs JavaScript inside MongoDB, the $function and $accumulator surfaces, and an honest look at what impact is reachable.

Published

The $where operator is MongoDB's escape hatch into real code. It evaluates a snippet of JavaScript for each document to decide whether it matches, and when an application builds that snippet from your input, you can inject JavaScript that runs inside the database engine. This is the most powerful NoSQL injection variant and the most misunderstood, so this page covers both the technique and an honest account of what it actually buys you. Practice on a free live target.

What $where is

$where lets a query run arbitrary JavaScript as a match condition. A legitimate use looks like this:

{ "$where": "this.age > 18" }

For each document, this is the document, and the expression returns true or false to include or exclude it. Because the expression is JavaScript, anything you can smuggle into it executes in the server's JavaScript context. Two related aggregation operators, $function and $accumulator (added in 4.4), expose the same server-side JavaScript surface inside aggregation pipelines and are worth checking wherever $where is blocked but aggregation is reachable.

Step 1: confirm server-side JavaScript is reachable

Before anything else, prove the clause runs. If you control a value that lands in a $where, force an always-true condition and see if the result set explodes to everything:

{ "$where": "1 == 1" }
{ "$where": "true" }

If there is no visible change, fall back to a measurable delay. Some builds expose a sleep() helper inside the $where context:

{ "$where": "sleep(5000) || true" }

sleep() is documented as a shell helper and is not guaranteed inside $where, so if it does nothing, use a clock busy loop, which needs no helper and is the portable form:

{ "$where": "var d=new Date(); while (new Date()-d < 5000) {} true" }

A five-second pause confirms both that your input reaches $where and that scripting is enabled. If neither the content nor the timing budges, scripting is likely disabled and you should rely on the operator and regex techniques instead.

Step 2: break out of the expression string

The injectable pattern is an app that concatenates your input into a JavaScript string it then runs. Picture a search that does:

const q = { "$where": "this.name == '" + req.query.name + "'" };

Your input sits inside single quotes. Close the quote and inject logic, the same instinct as SQL string breakout. A classic always-true:

' || '1'=='1

which makes the clause this.name == '' || '1'=='1', true for every document. To run a statement rather than just an expression, close the string, return your own value, and neutralise the trailing quote:

'; return true; var x='

Or target a specific document and confirm a fact about it:

'; return this.username == 'admin'; var x='

The var x=' at the end swallows the leftover closing quote from the original template so the JavaScript still parses.

Step 3: use the access you have, honestly

Inside $where you can:

  • Return true to match everything, a logic bypass that dumps a collection through an endpoint that filtered it.
  • Reference document fields with this, so you can test and leak values. Combined with timing, this.password.match(/^a/) && sleep(5000) reads a secret one character at a time, as covered in blind and timing.
  • Cause denial of service with an expensive expression, because the JavaScript runs per document and ties up the server. Do not do this to a shared target.

What you cannot normally do is touch the filesystem or spawn a process. The context is a JavaScript sandbox with no operating-system bindings, so there is no shell to pop. This is why the honest framing matters: $where injection is arbitrary JavaScript in the database, not a reverse shell. The realistic wins are logic bypass, extraction, and DoS. Actual OS-level code execution from here needs unusual conditions and is the exception, not the goal.

Why it is often unavailable

Server-side JavaScript is enabled by default on a self-hosted mongod, but the trend is firmly toward locking it down:

  • MongoDB 4.2.1 deprecated BSON JavaScript-with-scope for $where, and 4.4 added $function and $accumulator as the preferred replacements, part of a steady move away from server-side JavaScript.
  • MongoDB 5.0 deprecated the older map-reduce in favour of the aggregation pipeline.
  • Disabling scripting entirely (--noscripting or javascriptEnabled: false) is a standard hardening step, and managed providers frequently disable it.
  • $where, $function, and $accumulator are deprecated as of MongoDB 8.0, with $expr and aggregation expressions as the recommended replacement.

So test for it, because plenty of older or self-hosted deployments still have it on, but lead your assessment with operator and regex injection, which do not depend on scripting at all.

Fixing it

$where injection is the easiest NoSQL injection to eliminate structurally:

  • Never build a $where clause from user input. If you need per-document logic, express it with standard operators or $expr and bound values.
  • Disable server-side JavaScript with --noscripting, which turns off $where, $function, and $accumulator at once.
  • Validate types so a $where key cannot be smuggled in through a request body in the first place.

Full per-driver guidance is in preventing NoSQL injection. For every payload in one place, see the cheat sheet, or try $where on a free live target.

← Back to NoSQL Injection: The Complete Practical Guide to MongoDB Injection