NoSQL Injection: MongoDB Operator Abuse and Prevention Guide

NoSQL databases are not immune to injection attacks — they just look different. MongoDB operator injection via $where, $ne, and $regex operators allows authentication bypass and data exfiltration. This guide covers attack mechanics, code examples in Node.js and Python, and parameterised query patterns that prevent injection.

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

OperatorFunctionInjection risk
$neNot equalBypass equality checks
$gt, $gte, $lt, $lteComparisonBypass range checks
$in, $ninSet membershipBypass allowlist/blocklist checks
$regexPattern matchExtract data character by character
$whereJavaScript expressionServer-side JS execution (critical)
$exprAggregation expressionsComplex 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.