Skip to content

VNX-NODE-014: NoSQL injection in MongoDB

Reports a MongoDB query filter (find, findOne, update*, delete*, count, aggregate...) taken from req.query/body or Koa ctx, as the whole filter or as a member value, or a $where clause built from any request value. Cast or $eq-wrapped values, sanitised input, findById and SQL ORM where: filters are not reported.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-NODE-014
Severity High
SARIF level error
Kind sast
Languages JavaScript / TypeScript
CWE CWE-943
CAPEC CAPEC-676
ATT&CK T1190
CWSS vector CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H
Tags nosql mongodb injection database
Source internal/sast/rules/vnx-node-014.rego

Overview

This rule detects MongoDB query method calls, find(), findOne(), findOneAndUpdate(), updateOne(), deleteOne(), deleteMany(), where the filter argument is supplied directly from req.body or req.query. Unlike SQL injection, which manipulates a query string, NoSQL injection exploits the fact that MongoDB query filters are JavaScript objects: an attacker who can control the filter object can inject MongoDB query operators ($where, $gt, $ne, $regex) to bypass authentication, extract data conditionally, or match unintended documents. This is CWE-943 (Improper Neutralization of Special Elements in Data Query Logic).

Why This Matters

NoSQL injection via MongoDB operators is a well-documented and frequently exploited attack. The classic authentication bypass works because a developer writes User.findOne(req.body) expecting { username: "alice", password: "secret" }, but an attacker sends { "username": "admin", "password": { "$ne": "" } }. The $ne (not-equal) operator matches any user whose password is not an empty string, which is all of them, so the query returns the first admin user without knowing the password.

The $where operator is even more dangerous: it accepts a JavaScript expression that is evaluated server-side by the MongoDB JavaScript engine. An attacker who can inject a $where clause can execute arbitrary JavaScript within the database process context, potentially causing denial of service through infinite loops or leaking data through timing side channels.

Real-world consequences include complete authentication bypass, unauthorised data exfiltration, and cascading data destruction via operator-injected deleteMany filters. Because MongoDB drivers accept any JavaScript object as a query filter, there is no layer between user input and query execution when request bodies are passed directly.

OWASP ASVS v4: V5.3.4, Verify that the application protects against NoSQL injection attacks.

What Gets Flagged

The filter (first argument) must carry a body or query value, which a parser can turn into an operator object such as {"$ne": null}, or a $where clause must be built from any request value.

// FLAGGED: operator injection through a member value
const user = await User.findOne({
  username: req.body.username,
  password: req.body.password,
});
await User.deleteMany(req.body);

// FLAGGED: two lines
const { email } = req.body;
await db.collection('accounts').findOne({ email });

// FLAGGED: JavaScript injection in $where
db.orders.find({ $where: `this.orderId === '${id}'` });   // id = req.params.id

Not flagged: String(req.body.username), new ObjectId(req.params.id), { $eq: req.body.email }, mongoSanitize(req.query) or the express-mongo-sanitize middleware, findById, { where: { ... } } (Sequelize, TypeORM, Prisma), route params used as a member value, and users.find(u => ...).

Remediation

  1. Extract only the specific fields your query needs from the request object. Never spread or pass the entire req.body or req.query to a query method:

    // SAFE: extract and validate specific fields
    app.post('/login', async (req, res) => {
      const { username, password } = req.body;
    
      // Type-check: both must be strings
      if (typeof username !== 'string' || typeof password !== 'string') {
        return res.status(400).json({ error: 'Invalid input' });
      }
    
      const user = await User.findOne({ username });
      if (!user || !await bcrypt.compare(password, user.passwordHash)) {
        return res.status(401).json({ error: 'Invalid credentials' });
      }
      res.json({ token: generateToken(user) });
    });
  2. Enforce string types for all user-supplied filter fields. MongoDB operators are objects, not strings, so a simple typeof check blocks operator injection entirely:

    // SAFE: type guard blocks operator objects such as { $ne: null }
    function assertString(value, fieldName) {
      if (typeof value !== 'string') {
        throw new Error(`${fieldName} must be a string`);
      }
      return value;
    }
    
    app.get('/users', async (req, res) => {
      const email = assertString(req.query.email, 'email');
      const users = await User.find({ email });
      res.json(users);
    });
  3. Disable the MongoDB JavaScript engine at the server level. In mongod.conf, set security.javascriptEnabled: false. This removes the $where attack surface entirely and has no impact if you are not using server-side JavaScript:

    # mongod.conf
    security:
      javascriptEnabled: false
  4. Use a validation library to enforce schema shape before the query. Libraries like joi, zod, or express-validator can reject input objects containing unexpected keys or non-string values:

    // SAFE: zod schema rejects operator objects automatically
    import { z } from 'zod';
    
    const LoginSchema = z.object({
      username: z.string().min(1).max(64),
      password: z.string().min(1).max(128),
    });
    
    app.post('/login', async (req, res) => {
      const result = LoginSchema.safeParse(req.body);
      if (!result.success) return res.status(400).json({ error: 'Invalid input' });
      const { username, password } = result.data;
      // safe to query, only string values reach the DB layer
    });
  5. Sanitize MongoDB queries with mongo-sanitize as an additional defence-in-depth middleware layer that strips any key beginning with $:

    npm install mongo-sanitize
    const sanitize = require('mongo-sanitize');
    
    app.use((req, res, next) => {
      req.body = sanitize(req.body);
      req.query = sanitize(req.query);
      next();
    });

    Note: mongo-sanitize is a safety net, not a primary defence. Always validate and extract specific fields first.

References

Suppressing this rule

Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):

# nosec vnx-node-014
// nosec vnx-node-014

Or record an ignore rule: vulnetix ignore add --rule vnx-node-014 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-node-014.rego, cli/internal/sast/docs/vnx-node-014.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.