Skip to content

CWE-943: NoSQL Injection - JavaScript

Overview

NoSQL Injection in JavaScript/Node.js applications occurs when untrusted input is used to construct NoSQL database queries (MongoDB, Redis, CouchDB, etc.) without proper validation. Untrusted input can originate from HTTP requests, external APIs, databases, files, message queues, or any source outside the application's control. Attackers can exploit this to bypass authentication, extract sensitive data, modify database contents, or execute unauthorized operations.

Primary Defence: Use Mongoose schemas with strict type definitions and query builders instead of raw query object construction, validate and sanitize all user input before including in queries, explicitly reject or strip NoSQL operator characters ($, .), implement allowlists for permitted query fields and operators, and use parameterized queries or prepared statements where available to prevent NoSQL injection attacks.

Common Node.js NoSQL Vulnerabilities:

  • MongoDB query operator injection ($ne, $gt, $where, $regex)
  • MongoDB aggregation pipeline manipulation
  • Redis command injection
  • CouchDB Mango query injection
  • DynamoDB expression attribute injection

Popular Node.js NoSQL Libraries:

  • mongodb: Official MongoDB driver
  • mongoose: MongoDB ODM
  • ioredis / redis: Redis clients
  • nano: CouchDB client
  • aws-sdk: DynamoDB client

Common Vulnerable Patterns

MongoDB Operator Injection

// VULNERABLE - Direct untrusted input in MongoDB query
const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('app_database');

async function authenticateUser(username, password) {
    // VULNERABLE - Untrusted input directly in query
    const user = await db.collection('users').findOne({
        username: username,
        password: password
    });

    return user !== null;
}

// Attack: username = {$ne: null}, password = {$ne: null}
// Query becomes: {username: {$ne: null}, password: {$ne: null}}
// Returns first user (authentication bypass!)

Why this is vulnerable:

  • MongoDB operators ($ne, $gt) accepted in queries
  • JSON object injection from request body
  • No type validation
  • Authentication bypass

Express API with JSON Injection

// VULNERABLE - Accepting arbitrary JSON in queries
const express = require('express');
const { MongoClient } = require('mongodb');

const app = express();
app.use(express.json());

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shop');

app.post('/api/products', async (req, res) => {
    // VULNERABLE - Arbitrary query object from untrusted source
    const query = req.body.query || {};

    // No validation on query structure
    const products = await db.collection('products')
        .find(query)
        .toArray();

    res.json(products);
});

// Attack POST body: {"query": {"price": {"$gt": 0}, "admin_only": {"$ne": true}}}
// Bypasses access controls, retrieves admin products

Why this is vulnerable:

  • Accepts arbitrary query operators
  • No field allowlist
  • Can access hidden/admin fields
  • Data exfiltration

MongoDB $where Operator Injection

// VULNERABLE - JavaScript code injection via $where
const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('app');

async function findUsersByAge(age) {
    // VULNERABLE - String concatenation in $where
    const query = {
        $where: `this.age > ${age}`
    };

    const users = await db.collection('users')
        .find(query)
        .toArray();

    return users;
}

// Attack: age = "0; return true; //"
// Executes arbitrary JavaScript: this.age > 0; return true; //
// Returns all users regardless of age

Why this is vulnerable:

  • $where executes JavaScript on MongoDB server
  • String concatenation allows code injection
  • Denial of service possible
  • Data exfiltration

Mongoose with Unsafe Queries

// VULNERABLE - Mongoose with query injection
const express = require('express');
const mongoose = require('mongoose');

const User = mongoose.model('User', {
    username: String,
    email: String,
    role: String
});

const app = express();
app.use(express.json());

app.get('/api/user', async (req, res) => {
    const { username } = req.query;

    // VULNERABLE - Untrusted input directly in query
    const user = await User.findOne({ username }).exec();

    res.json(user);
});

// Attack: ?username[$ne]=admin
// Query becomes: {username: {$ne: 'admin'}}
// Returns first non-admin user

Why this is vulnerable:

  • Query string parameters parsed as objects
  • Mongoose doesn't sanitize operator injection
  • Privilege escalation
  • Data exposure

Redis Command Injection

// VULNERABLE - Redis key injection
const express = require('express');
const redis = require('redis');

const app = express();
const client = redis.createClient();

app.get('/cache/:key', async (req, res) => {
    const { key } = req.params;

    // VULNERABLE - Untrusted input in Redis key
    const value = await client.get(key);
    res.send(value || 'Not found');
});

app.post('/cache', express.json(), async (req, res) => {
    const { key, value } = req.body;

    // VULNERABLE - Command injection possible
    await client.set(key, value);
    res.send('OK');
});

// Attack: key = "test\r\nFLUSHDB\r\n"
// Injects Redis command to flush entire database

Why this is vulnerable:

  • CRLF injection in Redis protocol
  • Can execute arbitrary Redis commands
  • Database wipeout
  • Data exfiltration

MongoDB Aggregation Injection

// VULNERABLE - Aggregation pipeline with untrusted input
const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('analytics');

async function getUserStats(userId, sortField) {
    // VULNERABLE - Untrusted input in aggregation pipeline
    const pipeline = [
        { $match: { user_id: userId } },
        { $sort: { [sortField]: -1 } },
        { $limit: 10 }
    ];

    const results = await db.collection('events')
        .aggregate(pipeline)
        .toArray();

    return results;
}

// Attack: sortField = "$where"
// Can inject operators into pipeline

Why this is vulnerable:

  • Aggregation operators injectable
  • No field validation
  • Pipeline manipulation
  • DoS attacks

MongoDB Regex Injection

// VULNERABLE - Regex injection in queries
const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('app');

async function searchUsers(searchTerm) {
    // VULNERABLE - Untrusted input in regex without escaping
    const query = {
        username: { $regex: searchTerm, $options: 'i' }
    };

    const users = await db.collection('users')
        .find(query)
        .toArray();

    return users;
}

// Attack: searchTerm = ".*"
// Returns ALL users (DoS, data exfiltration)
// Attack: searchTerm = "(a+)+"
// ReDoS attack (Regular Expression Denial of Service)

Why this is vulnerable:

  • Regex patterns from untrusted sources
  • ReDoS attacks possible
  • Information disclosure
  • No escaping or limits

Next.js API Route Injection

// VULNERABLE - Next.js API route with query injection
// pages/api/users.js

import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI);

export default async function handler(req, res) {
    const { method, query } = req;

    if (method !== 'GET') {
        return res.status(405).end();
    }

    const db = client.db('app');

    // VULNERABLE - Query parameters directly in MongoDB query
    const users = await db.collection('users')
        .find(query)
        .toArray();

    res.json(users);
}

// Attack: /api/users?role[$ne]=user
// Returns all non-user accounts (admins, etc.)

Why this is vulnerable:

  • Next.js parses nested query params as objects
  • No validation on query structure
  • Authorization bypass
  • Data exfiltration

Secure Patterns

MongoDB with Type Validation

// SECURE - Strict type validation for MongoDB queries
const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('app_database');

function validateString(value, maxLength = 100) {
    if (typeof value !== 'string') {
        throw new Error('Expected string value');
    }

    if (value.length > maxLength) {
        throw new Error(`Value exceeds max length ${maxLength}`);
    }

    return value;
}

async function authenticateUser(username, password) {
    // SECURE - Validate input types
    const cleanUsername = validateString(username, 50);
    const cleanPassword = validateString(password, 100);

    // SECURE - Only string values allowed, no operators
    const user = await db.collection('users').findOne({
        username: cleanUsername,
        password: cleanPassword  // In production, use hashed passwords!
    });

    return user !== null;
}

// Attack attempts with objects/operators will fail type validation

Why this works: Strict type checking (typeof value !== 'string') prevents operator injection - the validateString() function ensures the input is actually a string primitive, rejecting objects or arrays that could contain MongoDB operators like {"$ne": null}. Length limits prevent DoS attacks through extremely long input strings. Type-safe queries with validated string values cannot be manipulated to inject operators, as MongoDB will treat them as literal string comparisons rather than special query operators like $ne, $gt, or $regex.

Express with Query Allowlist

// SECURE - Field allowlist and validation
const express = require('express');
const { MongoClient } = require('mongodb');

const app = express();
app.use(express.json());

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shop');

// SECURE - Define allowed query fields
const ALLOWED_FIELDS = {
    name: 'string',
    category: 'string',
    price_min: 'number',
    price_max: 'number'
};

function buildSafeQuery(params) {
    const query = {};

    for (const [field, value] of Object.entries(params)) {
        // SECURE - Only allow allowlisted fields
        if (!(field in ALLOWED_FIELDS)) {
            continue;
        }

        const expectedType = ALLOWED_FIELDS[field];

        // SECURE - Validate type
        if (typeof value !== expectedType) {
            continue;
        }

        // SECURE - Build safe query conditions
        if (field === 'price_min') {
            query.price = query.price || {};
            query.price.$gte = value;
        } else if (field === 'price_max') {
            query.price = query.price || {};
            query.price.$lte = value;
        } else {
            query[field] = value;
        }
    }

    return query;
}

app.get('/api/products', async (req, res) => {
    // SECURE - Build validated query
    const safeQuery = buildSafeQuery(req.query);

    const products = await db.collection('products')
        .find(safeQuery)
        .limit(100)
        .toArray();

    res.json(products);
});

app.listen(3000);

Why this works: Field allowlist (ALLOWED_FIELDS object) restricts queries to explicitly permitted fields, preventing attackers from querying sensitive fields or injecting unexpected properties. Type validation (typeof value !== expectedType) ensures each field receives the expected data type (string vs number), blocking attempts to inject MongoDB operators or arrays/objects. Controlled operator usage - the code explicitly constructs safe MongoDB operators ($gte, $lte) based on field names, rather than accepting arbitrary operators from user input. Result limits (.limit(100)) prevent DoS attacks from queries that would return massive datasets.

Mongoose with Schema Validation

// SECURE - Mongoose with strict schema validation
const express = require('express');
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        maxlength: 50,
        match: /^[a-zA-Z0-9_]+$/
    },
    email: {
        type: String,
        required: true,
        match: /^[\w.-]+@[\w.-]+\.\w+$/
    },
    role: {
        type: String,
        enum: ['user', 'admin']
    }
}, { strict: true });

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

const app = express();
app.use(express.json());

function validateUsername(username) {
    if (typeof username !== 'string') {
        throw new Error('Username must be a string');
    }

    if (!/^[a-zA-Z0-9_]{3,50}$/.test(username)) {
        throw new Error('Invalid username format');
    }

    return username;
}

app.get('/api/user', async (req, res) => {
    try {
        const username = validateUsername(req.query.username);

        // SECURE - Use validated string, schema enforces types
        const user = await User.findOne({ username }).exec();

        if (!user) {
            return res.status(404).json({ error: 'User not found' });
        }

        res.json(user);
    } catch (error) {
        res.status(400).json({ error: error.message });
    }
});

app.listen(3000);

Why this works: Mongoose schema validation enforces data types at the ODM layer - the type: String declarations ensure username and email are always strings, rejecting objects or arrays that could contain operators. match regex validation restricts field values to safe patterns (alphanumeric usernames, valid email format). enum constraints limit the role field to explicitly allowed values ('user', 'admin'), preventing injection of arbitrary roles. strict: true mode rejects any fields not defined in the schema, blocking attempts to inject additional properties like {"$where": "malicious code"} or admin_override. Input validation function provides defense-in-depth by re-validating before the query.

Redis with Input Sanitization

// SECURE - Redis with key and value validation
const express = require('express');
const redis = require('redis');

const app = express();
app.use(express.json());

const client = redis.createClient();

function validateRedisKey(key) {
    if (typeof key !== 'string') {
        throw new Error('Key must be a string');
    }

    // SECURE - Only allow alphanumeric, dash, underscore
    if (!/^[a-zA-Z0-9_-]{1,100}$/.test(key)) {
        throw new Error('Invalid key format');
    }

    return key;
}

function validateRedisValue(value) {
    if (typeof value !== 'string') {
        throw new Error('Value must be a string');
    }

    // SECURE - Remove CRLF to prevent command injection
    const cleanValue = value.replace(/[\r\n]/g, '');

    if (cleanValue.length > 10000) {
        throw new Error('Value too large');
    }

    return cleanValue;
}

app.get('/cache/:key', async (req, res) => {
    try {
        const cleanKey = validateRedisKey(req.params.key);
        const value = await client.get(cleanKey);
        res.send(value || 'Not found');
    } catch (error) {
        res.status(400).send(error.message);
    }
});

app.post('/cache', async (req, res) => {
    try {
        const cleanKey = validateRedisKey(req.body.key);
        const cleanValue = validateRedisValue(req.body.value);

        // SECURE - Use setex with expiration
        await client.setEx(cleanKey, 3600, cleanValue);

        res.send('OK');
    } catch (error) {
        res.status(400).send(error.message);
    }
});

app.listen(3000);

Why this works: Regex validation (/^[a-zA-Z0-9_-]{1,100}$/) enforces an allowlist of safe characters for Redis keys, blocking special characters that could be used in protocol injection attacks. CRLF removal (value.replace(/[\r\n]/g, '')) prevents command injection attempts where attackers inject newlines to execute additional Redis commands like FLUSHDB or CONFIG SET. Length limits prevent DoS attacks from extremely long keys (100 chars max) or values (10,000 chars max). Type checking ensures inputs are strings, not objects that could bypass validation. Automatic expiration (setEx with 3600 second TTL) limits the lifespan of cached data, reducing the impact of any successful cache poisoning.

Safe MongoDB Aggregation

// SECURE - MongoDB aggregation with field allowlist
const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('analytics');

// SECURE - Define allowed sort fields
const ALLOWED_SORT_FIELDS = ['timestamp', 'event_type', 'user_id'];

function validateUserId(userId) {
    if (typeof userId !== 'string') {
        throw new Error('User ID must be a string');
    }

    if (!/^[a-zA-Z0-9_-]{1,50}$/.test(userId)) {
        throw new Error('Invalid user ID format');
    }

    return userId;
}

async function getUserStats(userId, sortField) {
    // SECURE - Validate user ID
    const cleanUserId = validateUserId(userId);

    // SECURE - Validate sort field against allowlist
    if (!ALLOWED_SORT_FIELDS.includes(sortField)) {
        throw new Error(`Invalid sort field. Allowed: ${ALLOWED_SORT_FIELDS.join(', ')}`);
    }

    // SECURE - Build pipeline with validated values
    const pipeline = [
        { $match: { user_id: cleanUserId } },
        { $sort: { [sortField]: -1 } },
        { $limit: 100 }
    ];

    const results = await db.collection('events')
        .aggregate(pipeline)
        .toArray();

    return results;
}

Why this works: Field allowlist (ALLOWED_SORT_FIELDS array) restricts which fields can be used in $sort, preventing attackers from injecting operators or accessing sensitive fields. Input validation ensures the user ID matches a safe pattern (alphanumeric, dash, underscore) and rejects special characters. Explicit pipeline construction with validated values prevents operator injection - the $match, $sort, and $limit stages are built programmatically with safe values, not by accepting arbitrary pipeline objects from user input. Result limits ($limit: 100) cap the number of returned documents, preventing DoS from aggregations that would process massive datasets.

Next.js with Validation

// SECURE - Next.js API route with validation
// pages/api/users.js

import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI);

const ALLOWED_ROLES = ['user', 'moderator', 'admin'];

function validateRole(role) {
    if (typeof role !== 'string') {
        throw new Error('Role must be a string');
    }

    if (!ALLOWED_ROLES.includes(role)) {
        throw new Error('Invalid role');
    }

    return role;
}

export default async function handler(req, res) {
    const { method, query } = req;

    if (method !== 'GET') {
        return res.status(405).end();
    }

    try {
        const db = client.db('app');

        // SECURE - Build validated query
        const safeQuery = {};

        if (query.role) {
            safeQuery.role = validateRole(query.role);
        }

        if (query.username && typeof query.username === 'string') {
            safeQuery.username = query.username.substring(0, 50);
        }

        const users = await db.collection('users')
            .find(safeQuery)
            .limit(100)
            .toArray();

        res.json(users);
    } catch (error) {
        res.status(400).json({ error: error.message });
    }
}

Why this works: The implementation validates all query parameters before constructing the MongoDB query, ensuring only allowlisted fields are included. Type checking rejects any non-string username values, preventing $ne operator injection. Role values are validated against an allowlist using validateRole(), blocking any attempt to inject query operators. The substring(0, 50) truncation limits input size, while the limit(100) caps result sets. This multi-layered validation ensures that user input can never alter the query structure to execute unintended database operations.

Verification

To verify NoSQL injection protection:

  • Test operator injection: Send MongoDB operators in requests (e.g., { "username": { "$ne": null } }) and verify they're rejected
  • Verify authentication: Attempt login with operator injection payloads - should fail, not bypass authentication
  • Test field allowlist: Send queries with unauthorized fields (e.g., admin_only, __proto__) and confirm they're ignored
  • Check prototype pollution: Test for __proto__, constructor, and prototype in query parameters
  • Test regex patterns: Submit complex regex patterns to ensure no ReDoS (Regular Expression Denial of Service) vulnerabilities
  • Review query construction: Search codebase for direct use of req.query or req.body in database queries
  • Verify type validation: Ensure all query parameters are validated for expected types before use
  • Test $where operator: Confirm $where is not used or is properly protected if necessary

Additional Resources