MongoDB NoSQL Injection: Attack Techniques, Real-World Exploits & Prevention
NoSQL Injection Is Hiding in Your Express + MongoDB App
Most developers know about SQL injection. Far fewer know that MongoDB applications are vulnerable to their own class of injection attacks — NoSQL injection — and it's just as devastating.
| Impact | SQL Injection | NoSQL Injection |
|---|---|---|
| Authentication bypass | ✅ | ✅ |
| Data exfiltration | ✅ | ✅ |
| Data modification | ✅ | ✅ |
| Denial of service | ✅ | ✅ |
| Remote code execution | Sometimes | Yes (via $where) |
Attack Type 1: Operator Injection (Authentication Bypass)
The most common NoSQL injection. Exploits MongoDB query operators like $gt, $ne, $regex.
Vulnerable Code
// ❌ Express + Mongoose — VULNERABLE
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
if (user) {
res.json({ message: 'Login successful', token: generateToken(user) });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
The Attack
# Attacker sends JSON body:
curl -X POST https://target.com/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": {"$ne": ""}}'
# MongoDB interprets this as:
# db.users.findOne({ username: "admin", password: { $ne: "" } })
# This matches any admin user whose password is NOT empty — bypassing auth!
More Operator Injection Payloads
// Bypass with $gt (greater than)
{"username": "admin", "password": {"$gt": ""}}
// Bypass with $regex (match any password)
{"username": "admin", "password": {"$regex": ".*"}}
// Enumerate all usernames
{"username": {"$regex": "^a"}, "password": {"$ne": ""}}
{"username": {"$regex": "^b"}, "password": {"$ne": ""}}
// Match any user
{"username": {"$ne": ""}, "password": {"$ne": ""}}
Fix: Input Validation
// ✅ Validate input types BEFORE querying
import { z } from 'zod';
const loginSchema = z.object({
username: z.string().min(1).max(100),
password: z.string().min(1).max(200),
});
app.post('/login', async (req, res) => {
// This rejects objects like { "$ne": "" } — only strings allowed
const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ username });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// ✅ Compare hashed password — never query with raw password
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
res.json({ token: generateToken(user) });
});
Attack Type 2: JavaScript Injection via $where
MongoDB's $where operator executes JavaScript — creating a code injection vector.
// ❌ EXTREMELY DANGEROUS — user input in $where
app.get('/search', async (req, res) => {
const { name } = req.query;
const users = await User.find({
$where: \`this.name === '${name}'\`
});
res.json(users);
});
# Attacker extracts data via timing:
/search?name=a'; sleep(5000); var x='
# Attacker exfiltrates data:
/search?name=a'; var x=this.password; while(x[0]=='a'){sleep(100)}; var y='
Fix: Never use $where with user input
// ✅ Use standard query operators instead
app.get('/search', async (req, res) => {
const name = String(req.query.name || '').slice(0, 100);
const users = await User.find({ name: { $eq: name } });
res.json(users);
});
Attack Type 3: Aggregation Pipeline Injection
// ❌ Vulnerable — user controls aggregation stage
app.get('/stats', async (req, res) => {
const { groupBy } = req.query;
const stats = await Order.aggregate([
{ $group: { _id: \`$${groupBy}\`, total: { $sum: '$amount' } } }
]);
res.json(stats);
});
// Attacker: /stats?groupBy=password
// Returns all unique passwords grouped!
Fix: Allowlist aggregation fields
const ALLOWED_GROUP_FIELDS = ['status', 'category', 'region'];
app.get('/stats', async (req, res) => {
const groupBy = String(req.query.groupBy);
if (!ALLOWED_GROUP_FIELDS.includes(groupBy)) {
return res.status(400).json({ error: 'Invalid groupBy field' });
}
const stats = await Order.aggregate([
{ $group: { _id: \`$${groupBy}\`, total: { $sum: '$amount' } } }
]);
res.json(stats);
});
Mongoose-Specific Protections
// ✅ Mongoose sanitize plugin
import mongoSanitize from 'express-mongo-sanitize';
// Strips $ and . from req.body, req.query, req.params
app.use(mongoSanitize());
// ✅ Schema-level validation
const userSchema = new mongoose.Schema({
username: { type: String, required: true, maxlength: 100 },
email: { type: String, required: true, match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
});
// ✅ Explicitly use $eq for equality checks
User.findOne({ username: { $eq: userInput } });
NoSQL Injection Prevention Checklist
- Input validation with strict schemas (Zod, Joi)
-
express-mongo-sanitizemiddleware installed - Never use
$wherewith user input - Never query with raw password — always hash and compare
- Allowlist fields for sort, project, and groupBy operations
- Use
$eqoperator explicitly in queries - Disable server-side JavaScript (
--noscriptingflag) - Parameterize all query values
- Log and alert on queries containing operators in user input
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Threat Modeling for Developers: STRIDE, PASTA & DREAD with Practical Examples
Threat modeling is the most cost-effective security activity — finding design flaws before writing code. This guide covers STRIDE, PASTA, and DREAD methodologies with real-world examples for web, API, and cloud applications.
Building a Security Champions Program: Scaling Security Across Dev Teams
Security teams can't review every line of code. Security Champions embed security expertise in every development team. This guide covers program design, champion selection, training, metrics, and sustaining engagement.
The Ultimate Secure Code Review Checklist for 2025
A comprehensive, language-agnostic checklist for secure code reviews. Use this as your team's standard for catching vulnerabilities before they reach production.