
Prevent NoSQL Injection in Node.js and MongoDB
TL;DR: NoSQL injection lets attackers bypass authentication, steal data, or crash your MongoDB-powered Node.js app by injecting query operators like $ne, $gt, or $where through unsanitized input. According to OWASP, injection is the third most common web application vulnerability. Prevent it by validating input types, using express-mongo-sanitize, enabling Mongoose strict schemas, and never passing raw request data into queries.
What is NoSQL Injection?
NoSQL injection is an attack where a malicious user sends crafted input that manipulates your MongoDB queries to do something unintended. Instead of injecting SQL strings, attackers inject JavaScript objects containing MongoDB query operators like $ne (not equal), $gt (greater than), or $where (arbitrary JavaScript execution).
According to Imperva, NoSQL injection can enable an attacker to bypass authentication, extract or edit data, cause denial of service, or execute arbitrary code on the server. The attack surface is growing — MongoDB had 23 security vulnerabilities published in 2025 and 12 already in 2026, with an average severity score of 6.6 out of 10, according to Stack Watch.
How NoSQL Injection Works in Node.js
The most common NoSQL injection in Node.js happens when you pass req.body directly into a MongoDB query without checking the data type. Here is exactly how an attacker exploits this.
The Vulnerable Login Route
// VULNERABLE - do not use this pattern
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email, password });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
res.json({ message: 'Login successful', user });
});A normal request sends strings:
{ "email": "user@example.com", "password": "mypassword123" }But an attacker sends this:
{ "email": "admin@example.com", "password": { "$ne": "" } }MongoDB interprets { "$ne": "" } as "password is not equal to empty string," which is true for every user that has a password. The attacker logs in as any user without knowing the password.
The $where Operator Attack
The $where operator is even more dangerous because it executes arbitrary JavaScript on the MongoDB server:
{ "$where": "sleep(10000)" }This forces the MongoDB instance to run at 100% CPU for 10 seconds, creating a denial-of-service attack. According to Bright Security, $where injection can execute any JavaScript code within the database context.
Common MongoDB Injection Attack Patterns
Understanding the attack patterns helps you recognize and block them. Here are the most common operators attackers exploit:
| Operator | Attack Payload | What It Does |
|---|---|---|
$ne | { "password": { "$ne": "" } } | Matches all documents where password is not empty |
$gt | { "password": { "$gt": "" } } | Matches all documents where password is greater than empty string |
$regex | { "email": { "$regex": ".*" } } | Matches all email values via wildcard regex |
$where | { "$where": "this.isAdmin === true" } | Executes JavaScript to find admin accounts |
$or | { "$or": [{ "role": "admin" }] } | Injects additional query conditions |
$exists | { "password": { "$exists": true } } | Matches all documents with a password field |
According to a research dataset published in ScienceDirect, out of 400 analyzed NoSQL injection commands, 221 (55%) were malicious, showing how diverse and common these attack vectors are.
How to Prevent NoSQL Injection in Node.js
Prevention requires multiple layers. No single technique is enough on its own. Here are five defenses, ordered from most critical to supporting.
1. Validate Input Types Explicitly
The simplest and most effective defense is ensuring query values are the type you expect. If password should be a string, reject anything that is not a string.
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Type checking blocks object injection
if (typeof email !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input types' });
}
const user = await User.findOne({ email });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' });
res.json({ message: 'Login successful' });
});This blocks the { "$ne": "" } payload immediately because typeof { "$ne": "" } returns "object", not "string".
2. Use express-mongo-sanitize Middleware
The express-mongo-sanitize package strips MongoDB operators from req.body, req.query, and req.params automatically.
const express = require('express');
const mongoSanitize = require('express-mongo-sanitize');
const app = express();
app.use(express.json());
// Strips any keys starting with $ or containing .
app.use(mongoSanitize());This transforms { "password": { "$ne": "" } } into { "password": {} } before it reaches your route handler. You can also configure it to replace characters instead of removing them:
app.use(mongoSanitize({ replaceWith: '_' }));
// { "$ne": "" } becomes { "_ne": "" }| Option | Behavior | When to Use |
|---|---|---|
| Default | Removes $ and . keys entirely | Most applications |
replaceWith: '_' | Replaces prohibited characters | When you need to preserve data structure |
allowDots: true | Allows . but blocks $ | Apps that legitimately use dots in field names |
3. Use Schema Validation with Mongoose
Mongoose schemas act as a second layer of defense by enforcing field types at the model level:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
type: String,
required: true,
minlength: 8
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
});When a query contains { password: { "$ne": "" } }, Mongoose's strict mode casts the value. Since { "$ne": "" } cannot be cast to a String, Mongoose throws a CastError instead of executing the query.
4. Enable Mongoose sanitizeFilter Option
After CVE-2025-23061 exposed a bypass in Mongoose's populate match option, Mongoose introduced sanitizeFilter as a connection-level option:
mongoose.connect(process.env.MONGODB_URI, {
sanitizeFilter: true
});With sanitizeFilter: true, Mongoose wraps any query filter containing $ operators in a $eq check, neutralizing injection attempts at the driver level.
| Defense Layer | What It Blocks | Coverage |
|---|---|---|
| Type checking | Object payloads in expected string fields | Route level |
| express-mongo-sanitize | $ and . operators in all request data | Application level |
| Mongoose schema | Type mismatches and invalid values | Model level |
| sanitizeFilter | $ operators in query filters | Driver level |
5. Never Use $where or eval
Avoid $where in queries entirely. It executes arbitrary JavaScript on the MongoDB server and has no safe way to accept user input:
// NEVER do this
const results = await User.find({
$where: `this.name === '${req.query.name}'`
});
// Use standard query operators instead
const results = await User.find({
name: req.query.name
});According to OWASP's Node.js Security Cheat Sheet, you should also avoid eval(), setTimeout(string), setInterval(string), and new Function(string) — all of which execute arbitrary code from user input.
Input Validation with Joi
For production applications, pair type checking with a validation library like Joi to define exact input schemas:
const Joi = require('joi');
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(128).required()
});
app.post('/login', async (req, res) => {
const { error, value } = loginSchema.validate(req.body);
if (error) return res.status(400).json({ error: error.details[0].message });
// value.email and value.password are guaranteed strings
const user = await User.findOne({ email: value.email });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const isMatch = await bcrypt.compare(value.password, user.password);
if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' });
res.json({ message: 'Login successful' });
});Joi rejects any non-string value for email and password before your query logic runs. This eliminates the injection vector at the earliest possible point in the request lifecycle.
Complete Security Setup for Express + MongoDB
If you are building a website for your startup, securing your API from day one saves you from costly breaches later. Here is a production-ready middleware stack that combines all the defenses:
const express = require('express');
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const rateLimit = require('express-rate-limit');
const app = express();
// Parse JSON bodies
app.use(express.json({ limit: '10kb' }));
// Set security HTTP headers
app.use(helmet());
// Sanitize request data against NoSQL injection
app.use(mongoSanitize());
// Rate limiting for auth routes
const authLimiter = rateLimit({
max: 10,
windowMs: 15 * 60 * 1000,
message: 'Too many login attempts, try again in 15 minutes'
});
app.use('/api/auth', authLimiter);| Middleware | Purpose | Attacks Prevented |
|---|---|---|
express.json({ limit: '10kb' }) | Limits body size | Payload-based DoS |
helmet() | Sets secure HTTP headers | XSS, clickjacking, MIME sniffing |
mongoSanitize() | Strips MongoDB operators | NoSQL injection |
rateLimit() | Limits request frequency | Brute force, credential stuffing |
NoSQL Injection vs SQL Injection
If you are coming from a SQL background (see our guide on SQL vs NoSQL databases in system design), this comparison clarifies how the two injection types differ:
| Aspect | SQL Injection | NoSQL Injection |
|---|---|---|
| Attack vector | Malicious SQL strings | Malicious JavaScript objects |
| Payload format | ' OR 1=1 -- | { "$ne": "" } |
| Target | SQL queries (MySQL, PostgreSQL) | MongoDB queries, aggregation pipelines |
| Language exploited | SQL | MongoDB query operators, JavaScript |
| Prevention | Parameterized queries, prepared statements | Type validation, input sanitization, schema enforcement |
| Detection difficulty | Well-known, many WAF rules | Less known, fewer detection tools |
According to OWASP, NoSQL injection is often harder to detect because there is no standard query language — each NoSQL database has its own syntax, making generic WAF rules less effective.
FAQ
What is NoSQL injection in MongoDB?
NoSQL injection is an attack where malicious users send crafted objects containing MongoDB query operators (like $ne, $gt, $where) through unsanitized input fields. When these objects reach a MongoDB query without validation, they manipulate the query logic — bypassing authentication, extracting data, or causing server crashes.
How do I test my Node.js app for NoSQL injection?
Send JSON payloads with MongoDB operators to your API endpoints using tools like Postman or curl. Try { "field": { "$ne": "" } } on login forms and search inputs. If the server returns data instead of rejecting the input, you have a vulnerability. Automated tools like OWASP ZAP and Burp Suite can also scan for NoSQL injection.
Is Mongoose enough to prevent NoSQL injection?
Mongoose schemas with strict typing help by casting values to expected types, but they are not sufficient alone. The CVE-2025-23061 vulnerability showed that $where could be injected through Mongoose's populate match option even with schemas in place. Combine Mongoose with express-mongo-sanitize, explicit type checking, and the sanitizeFilter: true connection option for complete protection.
Does express-mongo-sanitize affect performance?
The performance impact is negligible. The middleware performs a shallow traversal of req.body, req.query, and req.params on each request, checking for keys starting with $ or containing .. For typical API payloads, this adds less than a millisecond of processing time per request.
Can NoSQL injection happen with other databases besides MongoDB?
Yes. NoSQL injection affects any NoSQL database that accepts structured query objects, including CouchDB, Cassandra, and Redis. However, MongoDB is the most targeted because of its popularity and its query operator syntax that maps directly to JavaScript objects, making injection through JSON request bodies straightforward.
Resources
- OWASP Testing for NoSQL Injection — Official OWASP testing methodology for NoSQL injection
- OWASP Node.js Security Cheat Sheet — Comprehensive security checklist for Node.js applications
- express-mongo-sanitize on npm — Middleware documentation and configuration options
- PortSwigger NoSQL Injection Guide — In-depth technical breakdown with lab exercises
- Snyk NoSQL Injection Lesson — Interactive lesson covering CVE-2025-23061 and Mongoose vulnerabilities
- MongoDB Security Alerts — Official MongoDB vulnerability announcements