Skip to content

CWE-943: Improper Neutralization of Special Elements in Data Query Logic (NoSQL Injection)

Overview

NoSQL Injection occurs when untrusted input is used to construct NoSQL database queries without proper validation or sanitization, allowing attackers to manipulate queries and access or modify data. Untrusted input can originate from HTTP requests, external APIs, databases, files, message queues, or any source outside the application's control.

Risk

High: Attackers can bypass authentication, extract sensitive data, or modify database contents by injecting malicious query fragments. This can lead to data breaches and loss of integrity.

Remediation Strategy

Use Parameterized Queries (Primary Defense)

  • Always use parameterized APIs or query builders for NoSQL operations
  • Avoid string concatenation or direct object construction from user input

Validate and Sanitize Input

  • Enforce strict type and format checks on all untrusted data
  • Reject unexpected fields and values

Apply Least Privilege to Database Accounts

  • Limit database user permissions to only required operations
  • Avoid using admin/root accounts for application queries

Monitor and Log Query Activity

  • Log suspicious or failed queries for review
  • Alert on anomalous query patterns

Remediation Steps

Core principle: Never concatenate untrusted input into NoSQL queries; use parameterization and strict schema/allowlists.

For detailed, language-specific examples see the Language-Specific Guidance section.

Identify NoSQL Injection Vulnerabilities

When reviewing security scan results:

  • Check query construction: Look for queries built with user input
  • Review object creation: Find query objects created from request data
  • Check operator usage: Identify MongoDB operators ($eq, $ne, $gt, etc.) from user input
  • Review JSON parsing: Find JSON.parse() or similar with untrusted data
  • Check aggregation pipelines: Look for pipeline stages built from user input

Vulnerable patterns:

// MongoDB with user input directly in query
const user = await db.users.findOne({ 
  username: req.body.username,  // Could be { $ne: null }
  password: req.body.password 
});

// Query built from JSON
const filter = JSON.parse(req.body.filter);  // Attacker controls query!
const results = await db.collection.find(filter);

// Aggregation with user input
const pipeline = [
  { $match: { type: req.query.type } },  // Could inject operators
  { $group: req.body.groupBy }  // Full control over grouping
];

Use Parameterized Queries and Type Validation (Primary Defense)

// VULNERABLE - accepts query operators
app.post('/login', async (req, res) => {
  const user = await db.users.findOne({
    username: req.body.username,  // Could be { $ne: null }
    password: req.body.password   // Could be { $regex: '.*' }
  });
  // Attack: { "username": {"$ne": null}, "password": {"$ne": null} }
  // Result: Logs in as first user without knowing credentials!
});

// SECURE - validate types and sanitize
app.post('/login', async (req, res) => {
  const username = req.body.username;
  const password = req.body.password;

  // Type validation - must be strings
  if (typeof username !== 'string' || typeof password !== 'string') {
    return res.status(400).json({ error: 'Invalid input' });
  }

  // Length validation
  if (username.length > 100 || password.length > 100) {
    return res.status(400).json({ error: 'Input too long' });
  }

  // Use explicit operators (not user input)
  const user = await db.users.findOne({
    username: { $eq: username },  // Explicit equality
    password: { $eq: password }
  });

  if (user) {
    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

// Better - hash passwords and use safe comparison
app.post('/login', async (req, res) => {
  const username = sanitizeString(req.body.username);
  const password = req.body.password;

  const user = await db.users.findOne({ username: username });

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Compare hashed passwords (prevents injection)
  const isValid = await bcrypt.compare(password, user.passwordHash);

  if (isValid) {
    res.json({ success: true, userId: user._id });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

Python (PyMongo):

from pymongo import MongoClient
import re

# VULNERABLE
username = request.json.get('username')  # Could be {"$ne": null}
user = db.users.find_one({'username': username})

# SECURE
def sanitize_input(value):
    """Ensure input is a string, not a dict with operators"""
    if not isinstance(value, str):
        raise ValueError("Expected string input")
    return value

username = sanitize_input(request.json.get('username'))
password = sanitize_input(request.json.get('password'))

user = db.users.find_one({
    'username': {'$eq': username},  # Explicit operator
    'password': {'$eq': password}
})

Reject Query Operators from User Input

// Allowlist allowed fields for search
const ALLOWED_SEARCH_FIELDS = ['name', 'email', 'department'];
const ALLOWED_OPERATORS = ['$eq', '$in'];  // Only safe operators

function sanitizeQuery(userQuery) {
  const clean = {};

  for (const [key, value] of Object.entries(userQuery)) {
    // Reject MongoDB operators ($ne, $gt, $where, etc.)
    if (key.startsWith('$')) {
      throw new Error(`Operator ${key} not allowed`);
    }

    // Only allow allowlisted fields
    if (!ALLOWED_SEARCH_FIELDS.includes(key)) {
      throw new Error(`Field ${key} not allowed`);
    }

    // Recursively check for operators in values
    if (typeof value === 'object' && value !== null) {
      for (const op in value) {
        if (!ALLOWED_OPERATORS.includes(op)) {
          throw new Error(`Operator ${op} not allowed`);
        }
      }
    }

    clean[key] = value;
  }

  return clean;
}

// Usage
app.get('/search', async (req, res) => {
  try {
    const query = sanitizeQuery(req.query);
    const results = await db.users.find(query).toArray();
    res.json(results);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

Use Schema Validation and ODM/ORM Libraries

// Mongoose schema with validation
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: { 
    type: String, 
    required: true,
    maxlength: 50,
    match: /^[a-zA-Z0-9_]+$/  // Only alphanumeric
  },
  email: { 
    type: String, 
    required: true,
    match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  },
  age: { 
    type: Number, 
    min: 0, 
    max: 150 
  }
});

const User = mongoose.model('User', userSchema);

// SECURE - Mongoose validates types
app.post('/users', async (req, res) => {
  try {
    // Mongoose rejects invalid types and operators
    const user = new User({
      username: req.body.username,  // Validated by schema
      email: req.body.email,
      age: req.body.age
    });

    await user.save();
    res.json({ success: true });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Safe queries with Mongoose
app.get('/users/:id', async (req, res) => {
  const userId = req.params.id;

  // Mongoose query builder - safe
  const user = await User.findById(userId)  // Type-safe
    .where('age').gte(18)  // Explicit operators
    .select('username email')  // Allowlist fields
    .exec();

  res.json(user);
});

Monitor and Log NoSQL Queries

const winston = require('winston');

// Log all database queries
function logQuery(operation, collection, query, result) {
  winston.info('NoSQL Query', {
    timestamp: new Date().toISOString(),
    operation: operation,
    collection: collection,
    query: JSON.stringify(query),
    resultCount: result.length || 0,
    userId: req.user?.id,
    ip: req.ip
  });

  // Alert on suspicious patterns
  const queryStr = JSON.stringify(query);
  if (queryStr.includes('$where') || 
      queryStr.includes('$regex') ||
      queryStr.includes('mapReduce')) {
    winston.warn('Suspicious NoSQL query detected', { query, ip: req.ip });
    alertSecurityTeam({ type: 'NoSQL Injection Attempt', query, ip: req.ip });
  }
}

// Wrapper for find operations
async function safeFi(collection, query, options = {}) {
  // Validate query doesn't contain dangerous operators
  const dangerousOps = ['$where', 'mapReduce', '$function'];
  const queryStr = JSON.stringify(query);

  for (const op of dangerousOps) {
    if (queryStr.includes(op)) {
      throw new Error(`Dangerous operator ${op} detected`);
    }
  }

  const result = await db.collection(collection).find(query, options).toArray();
  logQuery('find', collection, query, result);

  return result;
}

Test NoSQL Injection Protection

// Test payload
const testPayloads = [
  // Operator injection
  { username: { $ne: null }, password: { $ne: null } },

  // Regex injection
  { username: { $regex: '.*' } },

  // JavaScript execution (MongoDB)
  { $where: 'this.username == "admin"' },

  // Array injection
  { username: { $in: ['admin', 'root'] } },

  // Type confusion
  { age: { $gt: '' } }  // String instead of number
];

async function testInjectionProtection() {
  for (const payload of testPayloads) {
    try {
      const result = await db.users.findOne(payload);
      console.error(`✗ Injection succeeded with:`, payload);
    } catch (err) {
      console.log(`✓ Injection blocked:`, err.message);
    }
  }

  // Test legitimate query still works
  const legit = await db.users.findOne({ 
    username: 'testuser',
    age: 25 
  });
  console.log('✓ Legitimate query works:', legit !== null);
}

testInjectionProtection();

Common Vulnerable Patterns

  • Directly embedding user input in query objects
  • Accepting arbitrary JSON from clients
  • Failing to validate query parameters

Operator Injection in MongoDB Query (JavaScript)

// MongoDB example
const user = db.users.findOne({ username: req.body.username, password: req.body.password });
// Attacker can inject: { "$ne": null }

Secure Patterns

Type Validation and Sanitized Query (JavaScript)

// Use parameterized query and input validation
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
    throw new Error('Invalid input type');
}
const username = req.body.username;
const password = req.body.password;
const user = db.users.findOne({ username, password });

Why this works:

  • Validates input types to ensure strings only, preventing operator injection objects like {"$ne": null}
  • Rejects complex objects that could contain malicious NoSQL operators ($where, $regex, $gt)
  • Uses direct value assignment instead of allowing arbitrary query structure from user input
  • Prevents attackers from manipulating query logic to bypass authentication or access unauthorized data
  • Combined with schema validation, ensures database queries operate on expected data types only

Security Checklist

  • All user input validated for type (must be string/number, not object)
  • No MongoDB operators ($ne, $gt, $where, etc.) accepted from user input
  • Query fields allowlisted (only allowed fields can be queried)
  • Using ODM/ORM with schema validation (Mongoose, etc.)
  • Passwords hashed (prevents injection in password comparisons)
  • Dangerous operators ($where, mapReduce) blocked completely
  • Database queries logged and monitored
  • Tested with injection payloads (all should be rejected)
  • Least privilege database account (read-only where possible)

Language-Specific Guidance

For detailed, framework-specific code examples and patterns, see:

  • C# - MongoDB.Driver, Azure Cosmos DB with parameterized queries
  • Java - MongoDB Java Driver, Spring Data MongoDB with type validation
  • JavaScript - MongoDB driver, Mongoose, Express with query allowlisting
  • Python - PyMongo, Motor, mongoengine with operator injection prevention

Additional Resources