CWEE Module 02 Hero

Where NoSQL injection fits — and the one idea behind all of it

One idea, many channels

This is the second module in the Senior Web Penetration Tester path, the track that leads into CWEE. Module one was classic injection; this one is entirely about MongoDB and NoSQL injection, which looks nothing like SQLi on the surface and trips people up for exactly that reason.

There is no NoSQL “standard” the way there is a SQL standard, so there is no single NoSQL injection. What there is, in MongoDB specifically, is one underlying idea that every technique in the module turns out to be a variation on: the server builds a query from user input, and that input is trusted to be the type the developer assumed it was. Break that assumption — hand the query an object, or a fragment of JavaScript, where it expected a plain string — and you are controlling query logic instead of supplying a value.

Everything that follows (authentication bypass, data extraction, blind inference, server-side JS execution) is that same idea applied through different channels. Once it clicks, the module stops feeling like a grab-bag of tricks and starts feeling like one primitive with a lot of delivery mechanisms.


The core primitive: an object where a string was expected

An object where a string was expected

Picture a login handler that does roughly this:

db.users.find({ username: req.body.username, password: req.body.password })

The developer pictures username as a string. But if the request body is JSON, the client can send an object instead, and MongoDB will happily interpret that object as a set of query operators. So instead of {"username": "gerald"} you send {"username": {"$regex": ".*"}}, and the field that was meant to match one exact name now matches every name.

That single substitution is the whole thing. MongoDB is strongly typed — it treats 1 and "1" as different — but the languages in front of it often are not, and they pass whatever structure arrives straight into the query. The operators you lean on are the ordinary ones: $ne (not equal), $gt / $lt (comparisons), $regex (pattern match), $in, $where (the dangerous one, more on it below). None of them are exotic; the exploit is simply getting one of them to ride in where a string belonged.

The practical catch is the request shape. Operator-as-object injection only works when the body is parsed as JSON. If the form is application/x-www-form-urlencoded, you fall back to bracket notation — username[$ne]=x — which some frameworks expand into the same nested object. Whether that bracket trick works at all depends entirely on the framework, which turns out to matter a lot.


Operator injection in practice

Operator injection in practice

Three quick shapes the module drills, all the same primitive:

Authentication bypass. A login checking {email, password} falls to {"email": {"$ne": "x"}, "password": {"$ne": "x"}}. Both fields match anything-that-is-not-x, so the query returns a user and you are logged in. One nuance worth internalizing early: $ne logs you in as whichever document comes back first, which may not be the admin. When it matters, pin the field you know (email: "admin@...") and only inject the unknown one, or target directly with $regex: "^admin".

In-band data extraction. A search box running {name: <input>} dumps its whole collection when you make the filter match everything: {"name": {"$regex": ".*"}} or {"name": {"$gt": ""}}. “In-band” means the results come straight back in the response — the easy case.

The request-shape wrinkle, again. The same bypass is spelled differently depending on content type. JSON body: {"password": {"$ne": "x"}}. URL-encoded form: password[$ne]=x. Get this wrong and a perfectly injectable endpoint looks immune. The cleanest habit is to intercept one real request first and confirm what the app actually parses, rather than assuming a spelling.


Blind extraction: when the server stops answering

Blind extraction

This is where the module earns its difficulty tier. Often the data you want is never in the response — a login that only says “success” or “failed”, a lookup that only says “found” or “not found”. You cannot dump it. But you can ask yes/no questions and read the answer off the server’s behavior. That behavior is your true/false signal.

The workhorse is an anchored $regex. To pull a secret you cannot see, you guess it one character at a time:

{"field": {"$regex": "^A.*"}}   -> matches? first char is A
{"field": {"$regex": "^AB.*"}}  -> matches? second char is B

Each guess that flips the signal to TRUE confirms a character; then you extend and move on. Nobody hand-fuzzes this — you script it, which is the real skill the module is building. The anchor point is to key the signal on something only the TRUE response contains, so a near-miss cannot fool you.

Three refinements matter:

  • Know your charset and format. Extraction is only as good as the alphabet you loop over. Assume hex when the value is a hash, widen to full printable ASCII when a character stalls the loop — a silent stall usually means the real character is not in your set, not that you have reached the end.
  • Comparing by character code beats regex. Once the alphabet is wide, every guessed character has to be regex-escaped or it behaves as an operator (. matching anything is the classic false-positive). Switching the signal to this.field.charCodeAt(i) == c sidesteps escaping entirely — you compare integers, and backslashes, quotes and braces are all just numbers.
  • Binary search is the big win. Instead of “is it == each of ~95 characters”, ask “is charCodeAt(i) > mid” and halve the range. That is roughly 7 requests per character instead of up to 95 — on a real extraction it is the difference between minutes and seconds.

Server-Side JavaScript Injection: a sharper primitive

Server-Side JavaScript Injection

Everything above abuses query operators. Server-Side JavaScript Injection (SSJI) is a different and nastier beast. When an app builds a $where clause by string-concatenating user input into a JavaScript expression, you are no longer bending a query — you are executing JavaScript in the database’s context.

The vulnerable pattern looks like:

db.users.find({ $where: 'this.username === "' + input + '" && this.password === "..."' })

Because it is raw string concatenation, you break out of the quotes and inject your own logic. That is also why $ne and $regex stop working against a $where sink — there is no operator being parsed, it is a JS string. So when your object-injection payloads suddenly fail on an obviously-injectable login, “maybe it is $where” is the pivot.

Two things the module makes you feel the hard way:

  • JavaScript semantics decide whether a valid injection actually works. A syntactically perfect breakout can still fail on operator precedence — && binds tighter than ||, so a stray "1"=="1" can get trapped inside the password clause and evaluate false. Finding the sink is half of SSJI; reasoning about the expression you are injecting into is the other half.
  • It eats plain strings. This is the key reason SSJI survives defenses that stop operator injection: casting input to a string does nothing, because a string is exactly what the $where concatenation wants. The same charCodeAt binary-search extraction from the blind section drops straight onto a $where signal.

Tooling, and where it stops helping

Tooling, and where it stops helping

The module covers the usual automated options, and it is worth being honest about what they are good for.

Fuzzing with wordlists (e.g. wfuzz against a parameter with a NoSQLi payload list) is a discovery technique: fire many candidate payloads and watch for a response that behaves differently. The tell is usually a response-size anomaly — one payload comes back a different length from the rest, and you resend that one by hand to confirm. There are not many public NoSQL wordlists, but SecLists and a couple of dedicated MongoDB lists exist.

NoSQLMap automates discovery and confirmation — point it at a parameter and it runs a battery of operator and $where tests, reporting which ones look exploitable. It is a Python 2 tool, which is its own small adventure to install.

The honest framing: these tools answer “is this injectable, and with what?” They do not do the exploitation for you. None of them would cleanly dump a specific secret or walk a blind signal to completion the way a short bespoke script does. The module makes you build the extractors by hand first, on purpose, so that when a scanner flags an injection you actually understand what to do with it. Discovery and exploitation are different jobs, and the hand-built extractor is still the right tool for the second one.


Defense: no single silver bullet

Defense: no single silver bullet

The secure-coding section is a good counterweight, because NoSQLi prevention is genuinely less tidy than SQLi’s. There is no NoSQL equivalent of the parameterized query that just solves it. What you have instead is a layered discipline:

  • Cast input to the expected type. If a field should be a string, force it to a string before it reaches the query. In PHP that is strval(); the operator object {$ne: ...} collapses to the literal "Array" and the attack dies. This single step stops the whole operator-injection family.
  • Validate format on top of casting. Casting stops today’s attack; validation (an email regex, a tracking-number whitelist) stops the next bug you have not found. Defense in depth, not either/or.
  • Rewrite queries to avoid evaluating JavaScript. Casting does nothing for $where SSJI, because the exploit is a string. The fix there is to not build a $where JS string at all — express the same check with ordinary query operators. MongoDB’s own docs say to use $where only when a query is impossible any other way.
  • Disable server-side JavaScript if the application never needs it. It is on by default; turning it off removes the $where attack surface entirely. “Is server-side JS even enabled, and does it need to be?” is a fast, high-value finding on a real engagement.

The one-line version to hand a dev team: never put raw user input in a query, and avoid JavaScript-evaluating operators. Most queries can be written without them.


The lessons that actually transfer

Payloads are easy to look up. Judgment is the module.

The payloads are easy to look up. The judgment is what the module is really teaching, and four things carried further than any specific technique:

Confirm the injection channel before concluding it is not vulnerable. A clean-looking payload returning a clean 200 can mean the app never read your input at all. Framework fingerprinting is what turns that dead end into the next move: a Python/Flask app reads form values as strings, so a JSON operator body may never populate the field the code looks at. Read the error strings — “missing parameter” and “invalid value” point at completely different problems.

Read which layer failed. A 500 versus a 200, one error message versus another, a one-byte difference in response length — each points at a different layer: HTTP parsing, JS syntax, query logic, or result rendering. Diagnosing which layer broke, instead of mashing the next payload, is the single habit that separates reliable exploitation from guesswork.

An injection’s value may be reading, not bypassing. The reflex is “how do I log in.” But a blind read over a document is more powerful than an auth bypass — it lets you read any field of any record, not just impersonate one user. Recognizing when the injection is better used to read data than to bypass a check is a more mature instinct, and sometimes the only one that works.

A hardened-looking artifact can be deliberate misdirection. When a recovered hash turns out to be slow-by-design (bcrypt, high cost factor) and cracking looks intractable, that is often a signal that cracking is not the intended path — and that the same injection hands you a faster one. Do not grind the obvious route when the vulnerability is offering a shortcut.

That is module two. Same shape as the series so far: technique notes and the reasoning behind them, no flags or assessment spoilers.

Module 2 done — No tables, no problem, right?, 6 November 2026.

No tables, no problem, right? — Introduction to NoSQL Injection module badge

On to the next one.