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:
$whereexecutes 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, andprototypein 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.queryorreq.bodyin database queries - Verify type validation: Ensure all query parameters are validated for expected types before use
- Test
$whereoperator: Confirm$whereis not used or is properly protected if necessary