NoSQL Injection: The Complete Practical Guide to MongoDB Injection
NoSQL Injection in URLs and Form Bodies
How body parsers turn bracket-notation parameters into operator objects, letting NoSQL injection reach GET and form endpoints that never accept JSON.
Published
Operator injection is easy to picture in a JSON API, where you obviously control the type of each field. The surprise is that the same attack reaches endpoints that only ever see ordinary form or query-string parameters. The reason is the body parser: several popular parsers turn bracket-notation parameters into nested objects, so a flat-looking request can arrive at the query as an operator. This page shows how that happens and how to exploit it. Practice on a free target as you read.
The parser is the vulnerability
When a browser submits a form or you hit a URL with a query string, the server parses that text into a structure the app can use. The question is what the parser does with a parameter name that contains square brackets. Many parsers interpret brackets as structure. So this request body:
username=admin&password[$ne]=xis not parsed as a parameter literally named password[$ne]. It is parsed into a nested object:
{ "username": "admin", "password": { "$ne": "x" } }The qs library, which Express uses by default for extended URL-encoded bodies (with the extended option on), does exactly this. PHP's array syntax does the equivalent. The developer wrote code expecting password to be a string. The parser handed them an object with a $ne operator inside. The MongoDB driver sees the operator and obeys. No JSON was ever sent, yet operator injection landed.
Step 1: spot a non-JSON endpoint
Look for the requests you would normally ignore because they are "just a form" or "just a search." Any of these is a candidate if the parameter reaches a query:
- A login that posts
application/x-www-form-urlencodedinstead of JSON. - A search box that issues a GET with a query parameter.
- A filter, a lookup by email, a password-reset request.
Send a baseline request and record the normal response, the same way you would for JSON operator injection.
Step 2: rewrite a parameter with bracket notation
Convert a flat parameter into a bracketed one so the parser builds an operator. The always-true not-equal payload in form-body form:
username=admin&password[$ne]=xThe same idea in a GET query string:
/login?username=admin&password[$ne]=xAnd a widely useful variant that matches the first user in the collection:
username[$ne]=x&password[$ne]=xEach of these mirrors a JSON operator payload exactly, only expressed in bracket notation so the server parser rebuilds the object for you.
Step 3: confirm the parser cooperated
Watch the response. If it flips from your baseline failure to a success, the parser built the operator object and the driver obeyed, confirming both the parsing behaviour and the injection in one shot. If nothing changes, the parser may be flattening brackets into literal string keys, in which case fall back to a JSON body if the endpoint accepts one, or move on.
Refining with regex and anchoring
Everything you can do in JSON you can do here. Anchor a username to a specific account:
username[$regex]=^admin&password[$ne]=xBegin a blind password extraction by testing a prefix, where a change in the response tells you the guess was right:
username=admin&password[$regex]=^aThat regex form is the bridge into blind and timing, where you script the one-character-at-a-time recovery.
URL encoding
Characters like $, [, ], and ^ are legal in many contexts but safest to percent-encode when a proxy or client mangles them. $ is %24, [ is %5b, ] is %5d, ^ is %5e. The decoded request is identical to the readable form above, so encode only what you must to get the bytes through intact.
Why GET endpoints matter
An injectable GET parameter is worse than a form, because it travels in URLs. That means it can land in browser history, server logs, and referer headers, and it can sometimes be triggered cross-site. The core bug is the same operator injection, but the reach is wider, which is a good reason to test search and lookup endpoints and not just logins.
Fixing it
The parser behaviour is not itself the bug, the missing type check is. Defenses:
- Validate types after parsing. A value that should be a string but arrived as an object or array is an attack, reject it.
- Configure the parser conservatively where you can, but never rely on parser configuration as the only control, because the real fix is type validation.
- Pull and cast named fields rather than passing the parsed body straight into a query.
Full guidance is in preventing NoSQL injection. Next, turn these operators into a data-reading tool in data extraction, or try the bracket payloads on a free live target.
← Back to NoSQL Injection: The Complete Practical Guide to MongoDB Injection