NoSQL Injection Is Not the Same as SQL Injection
Developers who moved from relational databases to NoSQL often carry the mental model that parameterised queries were the SQL-specific fix, and that document stores are inherently safer. This is wrong. NoSQL databases accept structured queries — typically JSON or command strings — and when user input is interpolated into those structures unsanitised, the attack surface changes shape but doesn’t disappear.
The injection patterns differ per database: MongoDB’s operator injection manipulates query logic, Redis command injection exploits server-side Lua scripting and client library behaviour, and Elasticsearch’s dynamic query construction exposes search-time code execution. Each requires different prevention strategies.
MongoDB Operator Injection
How it works
MongoDB queries are JSON documents. The query language includes operators like $where, $gt, $ne, $in, and $regex that modify query semantics. When user-supplied JSON is directly embedded in a query, an attacker can inject these operators to alter query logic.
Authentication bypass — the classic pattern:
// VULNERABLE — Express/Node.js
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Attacker sends: { "username": {"$ne": null}, "password": {"$ne": null} }
const user = await db.collection('users').findOne({
username: username, // becomes { "$ne": null } — matches ALL documents
password: password // becomes { "$ne": null } — always true
});
if (user) {
// Authentication bypassed — logs in as the first user in the collection
return res.json({ token: generateToken(user) });
}
});
The attacker sends a POST body of {"username": {"$ne": null}, "password": {"$ne": null}} — valid JSON that Express parses into objects. MongoDB receives operators instead of strings and returns the first matching document.
$where JavaScript injection:
// VULNERABLE — $where allows arbitrary JavaScript execution
app.get('/search', async (req, res) => {
const results = await db.collection('products').find({
$where: `this.name == '${req.query.name}'`
// ^^^^^^^^^^^^^^^^ string interpolation into JS
// Attacker: name='; sleep(5000); var x='
// Results in: this.name == ''; sleep(5000); var x=''
}).toArray();
});
$where executes JavaScript in MongoDB’s server-side JS engine. Command injection here can cause DoS, data exfiltration via blind boolean-based injection, or information disclosure.
Prevention
// FIXED — enforce types, use typed schemas
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Type enforcement: reject non-string input
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
// Sanitise: strip MongoDB operators from strings
const sanitise = (str) => str.replace(/[\$\.]/g, '');
const user = await db.collection('users').findOne({
username: sanitise(username),
password: sanitise(password)
});
if (user && await bcrypt.compare(password, user.passwordHash)) {
return res.json({ token: generateToken(user) });
}
res.status(401).json({ error: 'Invalid credentials' });
});
# Python with pymongo — safe query construction
from bson import ObjectId
def get_user(username: str, password: str):
# Type check
if not isinstance(username, str) or not isinstance(password, str):
raise ValueError("Username and password must be strings")
# Do NOT pass raw dict from request — build query explicitly
user = db.users.find_one({
"username": username, # literal string, not a parsed dict
"active": True
})
if user and bcrypt_check(password, user["password_hash"]):
return user
return None
Never use $where with user input. Replace JavaScript-based queries with native MongoDB aggregation pipeline operators ($expr, $match, $filter). Disable server-side JavaScript entirely if not needed: mongod --noscripting.
Use mongoose schemas with sanitizeFilter:
const mongoose = require('mongoose');
mongoose.set('sanitizeFilter', true);
// Automatically strips $ and . from query keys
Redis Command Injection
How it works
Redis is a command-based protocol, not a query language. Injection risks arise in two contexts: client library command construction with unsanitised input, and server-side Lua scripting via EVAL.
Lua injection via EVAL:
# VULNERABLE — Python with redis-py
import redis
r = redis.Redis()
def get_user_score(username: str):
# Attacker input: "test\r\nDEL important_key\r\n"
lua_script = f"""
return redis.call('ZSCORE', 'leaderboard', '{username}')
"""
# CRLF injection can break out of Lua into raw Redis commands
return r.eval(lua_script, 0)
Raw string interpolation into Lua allows CRLF injection or Lua code injection depending on how the client processes the script.
Pipelining and MULTI/EXEC injection:
// VULNERABLE — constructing raw Redis commands from user input
const command = `SET user:${userId}:session ${sessionData}`;
redisClient.sendCommand(command.split(' '));
// If userId is "foo\r\nFLUSHALL\r\n", the pipeline executes FLUSHALL
Prevention
# FIXED — use parameterised EVAL with KEYS and ARGV
import redis
r = redis.Redis()
def get_user_score(username: str):
# username passed as ARGV[1], not interpolated into script
lua_script = """
return redis.call('ZSCORE', KEYS[1], ARGV[1])
"""
return r.eval(lua_script, 1, 'leaderboard', username)
# ^ ^key count ^key ^arg (username)
// FIXED — use structured command arrays, never string concatenation
const result = await redisClient.zScore('leaderboard', username);
// ioredis and node-redis handle serialisation safely when using typed methods
Key rules for Redis:
- Always pass user input as
KEYS/ARGVparameters to EVAL, never interpolate into Lua source - Use typed client methods (
GET,SET,ZADD) rather than raw command construction - Enable Redis AUTH and ACL rules to limit which commands can be executed per client
- Disable dangerous commands in production:
rename-command FLUSHALL ""inredis.conf
Elasticsearch Query Injection
How it works
Elasticsearch uses a JSON-based Query DSL. Applications that construct queries by concatenating user strings into JSON, or that expose the _search endpoint directly, are vulnerable to query manipulation.
Search endpoint exposure:
# VULNERABLE — constructing ES query with string formatting
def search_products(query: str):
es_query = f"""
{{
"query": {{
"query_string": {{
"query": "{query}"
}}
}}
}}
"""
# Attacker: query = '" } } } }' (closes the JSON, injects new clauses)
response = es.search(index='products', body=json.loads(es_query))
query_string abuse:
The query_string query type accepts Lucene syntax including wildcards, field-scoped searches, and Boolean operators. Even when the JSON is valid, an attacker can:
- Access fields they shouldn’t:
query=_source.password:* - Perform wildcard scans to extract data:
query=ssn:* - Cause ReDoS with complex regex:
query=/a.*a.*a.*a.*a.*a.*a.*a.*Z/
Prevention
# FIXED — use match query instead of query_string
from elasticsearch import Elasticsearch
es = Elasticsearch()
def search_products(query: str, category: str):
# Validate inputs
if not isinstance(query, str) or len(query) > 200:
raise ValueError("Invalid query")
# Use 'match' — not 'query_string' — to avoid Lucene injection
response = es.search(
index='products',
body={
"query": {
"bool": {
"must": [
{"match": {"name": query}}, # safely handled
],
"filter": [
{"term": {"category": category}},
{"term": {"active": True}}
]
}
},
"_source": ["id", "name", "price", "category"], # field allowlist
"size": 20
}
)
return response['hits']['hits']
// Java with Elasticsearch High Level REST Client
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(
QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", userInput)) // safe
.filter(QueryBuilders.termQuery("active", true))
)
.fetchSource(new String[]{"id", "name", "price"}, null) // field allowlist
.size(20);
Key rules for Elasticsearch:
- Never use
query_stringwith user input — usematch,term,range, ormulti_match - Define
_sourcefield allowlists to prevent sensitive field exfiltration - Disable
_allfield mapping (deprecated in ES7, but verify if upgrading from older clusters) - Do not expose the
_searchAPI endpoint directly to the internet — proxy through application logic - Use Elasticsearch security (X-Pack) with document-level security if tenants share an index
General Principles Across NoSQL Databases
Validate types before query construction. Every user-supplied value should be asserted as the expected type — string, integer, ObjectId — before being passed to any query method.
Build queries with driver methods, not string concatenation. MongoDB’s find(), Redis’s typed commands, and Elasticsearch’s query builder DSL all handle escaping correctly when used as designed. String interpolation into query text bypasses these protections.
Minimise query capability exposed to users. The search interface users interact with should accept a narrow, well-defined set of parameters — not a passthrough to the underlying query language. Treat every query parameter as untrusted input that needs mapping to a safe, predefined query structure.
Disable unused features. MongoDB $where and $function require server-side JavaScript — disable it if not needed. Redis’s EVAL, DEBUG, and CONFIG commands can be renamed or restricted. Elasticsearch’s query_string is powerful but rarely necessary in application search implementations.