Application Security
mongodb
nosql injection
express
mongoose
+3 more

MongoDB NoSQL Injection: Attack Techniques, Real-World Exploits & Prevention

SCRs Team
February 26, 2026
14 min read
Share

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.

ImpactSQL InjectionNoSQL Injection
Authentication bypass
Data exfiltration
Data modification
Denial of service
Remote code executionSometimesYes (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-sanitize middleware installed
  • Never use $where with user input
  • Never query with raw password — always hash and compare
  • Allowlist fields for sort, project, and groupBy operations
  • Use $eq operator explicitly in queries
  • Disable server-side JavaScript (--noscripting flag)
  • Parameterize all query values
  • Log and alert on queries containing operators in user input

Advertisement