NoSQL databases don’t use SQL, but they still accept attacker-controlled data in query structures — and that creates injection vulnerabilities. The mechanics differ from SQL injection, but the impact is comparable: authentication bypass, unauthorised data access, and in some configurations, server-side code execution.
MongoDB is the most widely deployed NoSQL database and the most studied for injection vulnerabilities. The patterns here apply equally to Firestore (different syntax, same structural issue) and most document store databases.
How MongoDB Query Operators Create the Injection Surface
MongoDB queries are expressed as JSON objects rather than SQL strings. A simple login query looks like this:
// Node.js / Mongoose
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
This looks safe because there’s no string concatenation. But the vulnerability comes from MongoDB’s query operators. If an attacker sends a JSON body where password is not a string but a query operator object:
{
"username": "admin",
"password": { "$ne": null }
}
The query becomes:
User.findOne({ username: "admin", password: { $ne: null } })
Which finds the first user named “admin” where the password is not null — which is always true. This is authentication bypass via operator injection.
Affected Operators
| Operator | Function | Injection risk |
|---|---|---|
$ne | Not equal | Bypass equality checks |
$gt, $gte, $lt, $lte | Comparison | Bypass range checks |
$in, $nin | Set membership | Bypass allowlist/blocklist checks |
$regex | Pattern match | Extract data character by character |
$where | JavaScript expression | Server-side JS execution (critical) |
$expr | Aggregation expressions | Complex condition bypass |
The $where Operator: Code Execution Risk
$where allows a JavaScript expression that is evaluated on the server:
// Vulnerable
db.users.find({ $where: `this.username == '${username}'` })
If username is 'a'; return true; var x=', the injected expression returns true for every document — full collection disclosure. Worse, $where expressions run in the MongoDB JavaScript engine, historically enabling server-side code execution in older MongoDB versions.
Mitigation: Disable $where entirely. In MongoDB 4.4+, use --noscripting flag at startup. In Mongoose:
mongoose.set('useFindAndModify', false);
// Restrict $where in schema validation
Practical Attack Scenarios
Authentication Bypass (Node.js / Express)
Vulnerable code:
app.post('/login', async (req, res) => {
const { username, password } = req.body; // No type checking
const user = await User.findOne({ username, password });
if (user) return res.json({ token: generateToken(user) });
return res.status(401).json({ error: 'Invalid credentials' });
});
Attack payload (JSON body):
{
"username": "admin",
"password": { "$gt": "" }
}
Because every password string is greater than an empty string, this returns the admin user without knowing the password.
Fixed code:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Type validation — reject any non-string values
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await User.findOne({ username });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// Use bcrypt comparison, not DB-level password query
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
return res.json({ token: generateToken(user) });
});
The fix does two things: validates that inputs are strings (not objects), and moves password comparison out of the database query entirely using bcrypt.
Regex-Based Data Extraction (Python / PyMongo)
Blind NoSQL injection works similarly to blind SQL injection: an attacker uses $regex to test conditions character by character and infer data from the response.
Vulnerable:
# Flask / PyMongo — search endpoint
@app.route('/search')
def search():
query = request.args.get('q', '')
# Direct insertion of user input into query
results = db.users.find({'email': {'$regex': query}})
return jsonify(list(results))
Attack: if an attacker can supply ^admin, they test whether any email starts with “admin”. By iterating character by character, they reconstruct email addresses from the database without reading them directly.
Fixed:
import re
from bson import Regex
@app.route('/search')
def search():
query = request.args.get('q', '')
# Validate input: only allow safe characters for search
if not re.match(r'^[a-zA-Z0-9 @._-]{1,100}$', query):
return jsonify({'error': 'Invalid search term'}), 400
# Use literal string match, or escape regex metacharacters
safe_query = re.escape(query)
results = db.users.find(
{'email': {'$regex': safe_query, '$options': 'i'}},
{'email': 1, 'name': 1, '_id': 0} # Projection — limit returned fields
)
return jsonify(list(results))
Structural Prevention Patterns
1. Type validation before queries
Reject any request body field that is an object when a scalar is expected:
// Generic input sanitiser for MongoDB queries
function sanitiseInput(input) {
if (typeof input === 'object' && input !== null) {
throw new Error('Object input not permitted in this context');
}
return input;
}
// Or use a schema validation library (Joi, Zod, Yup)
const schema = Joi.object({
username: Joi.string().alphanum().max(50).required(),
password: Joi.string().min(8).max(128).required()
});
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).json({ error: 'Validation failed' });
2. Disable dangerous operators at the middleware level
For Express applications, add a middleware that strips MongoDB operator keys from request bodies:
function sanitiseMongo(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
for (const key of Object.keys(obj)) {
if (key.startsWith('$')) {
delete obj[key];
} else {
obj[key] = sanitiseMongo(obj[key]);
}
}
return obj;
}
app.use((req, res, next) => {
if (req.body) req.body = sanitiseMongo(req.body);
if (req.query) req.query = sanitiseMongo(req.query);
next();
});
This is a defence-in-depth measure — it shouldn’t replace type validation, but it catches operator injection that slips through.
3. Use parameterised methods where available
Mongoose’s findById() and similar methods that take typed parameters are safer than findOne() with a constructed object. When building dynamic queries, use explicit schema validation rather than passing request data directly.
4. Projection — never return more than needed
Always use MongoDB projections to limit which fields queries return. If an injection does occur, projection limits the data exposed:
// Bad: returns all fields including passwordHash
User.findOne({ username })
// Good: explicit projection
User.findOne({ username }, { username: 1, email: 1, role: 1, _id: 0 })
Firebase / Firestore
Firestore uses a different query model that doesn’t have the same operator injection risk as MongoDB, but it has a related vulnerability: client-side rule bypass. If you’re calling Firestore directly from a client application, your security rules are the only access control. Misconfigured rules that use client-provided data in conditions (e.g., request.auth.token.role == resource.data.role) can be abused by supplying crafted tokens.
Keep Firestore rules simple: authenticate the user, check ownership or an enumerated role, and don’t build complex logic into rules that client-supplied data can influence.
Testing Your Application
Use the OWASP NoSQL Injection Testing Guide (OTG-INPVAL-005.6) as a baseline. Quick manual tests for any MongoDB-backed login or search endpoint:
# Test operator injection in JSON body
{"username": "admin", "password": {"$gt": ""}}
{"username": {"$ne": null}, "password": {"$ne": null}}
# Test in URL parameters (some frameworks auto-parse array/object notation)
?username[$ne]=&password[$ne]=
?search[$regex]=.*
If any of these return a successful response where you expect a login failure or empty results, you have a vulnerability.