MongoDB aggregation injection — $facet allowlist bypass → cross-collection read
SkillDatabases & dataBypass a MongoDB aggregation-pipeline stage allowlist by nesting disallowed read stages inside $facet, then $unionWith/$lookup sibling collections to exfiltrate secrets (invite tokens, creds, hashes). Load when: an endpoint accepts a user-supplied `pipeline` (or errors like "operator-form queries not accepted, use the pipeline parameter"), a Node/Express + MongoDB backend, 24-hex `_id`s, or an "advanced query" API. Authorized targets only.
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the MongoDB aggregation injection skill
What this skill tells your AI
The instructions your AI receives, as published by noorqureshi/sploitagent in skills/api/api-mongo-agg-facet-bypass/SKILL.md and read by ahel’s review.
When it applies
- An API runs a user-controlled aggregation pipeline on a fixed collection
(
db.collection('x').aggregate(userPipeline)), usually exposed as apipelinequery/body parameter for "advanced" search. - The server defends with a stage allowlist — only screens the top-level stage
names (
$match/$project/$sort/$limit/$facetallowed;$lookup/$unionWith/$group/…rejected with something like"invalid or disallowed pipeline stage"). - Tell-tale that a
pipelineparam even exists: sending the normal search term as a Mongo operator object (?q[$ne]=x) returns a hint such as"Operator-form queries not accepted on 'q'. Use the 'pipeline' parameter…".
Why it works
The allowlist inspects only the outermost stage keys. $facet runs sub-pipelines
whose stages are never re-screened by the app, so a disallowed read stage placed
inside a $facet sub-pipeline reaches MongoDB unchecked. MongoDB itself still forbids
a few stages inside $facet ($out/$merge/$collStats/$indexStats/$listCatalog/$documents),
but it permits $lookup and $unionWith there — and those read other collections
in the same database. That turns a "search our metadata" endpoint into "read any
collection in this DB".
Method
- Confirm the pipeline sink & allowlist. Baseline
?pipeline=[{"$limit":1}]returns docs;?pipeline=[{"$count":"n"}]/$group/ top-level$unionWith→ "disallowed stage". - Leak the namespace. Trigger a Mongo error (e.g.
$facetcontaining a stage Mongo rejects) — the 500 body usually includes"ns":"<db>.<collection>". Now you know the DB. - Read a sibling collection (drop the base docs first so output is only the target):
[{"$facet":{"r":[ {"$match":{"<anyfield>":"__none__"}}, {"$unionWith":{"coll":"<target_collection>","pipeline":[{"$limit":20}]}} ]}}]$unionWithhere is legal ONLY inside$facet(top-level → allowlist rejects it). - Enumerate collection names you don't know: build one
$facetwith many sub-pipelines, each$unionWith-ing a candidate name +$limit:1; names that return docs exist & are non-empty. Brute the same naming style as any collection you already know (e.g. knownmds_entries⇒ trypending_invites,operator_accounts,invite_tokens). - Loot the target collection (invite tokens, password hashes, session docs, API keys) and pivot (register/login, crack hashes, forge sessions).
Tools
curl -G --data-urlencode 'pipeline=…'(single-quote so the shell doesn't eat$), or a short Pythonurllibhelper that JSON-encodes and URL-encodes the pipeline (cleaner for building big$facetmaps and parsing results). Seeaegis/exploit-dev/*.pyfor a worked helper.
Gotchas
- Shell
$expansion:"?q[$ne]=x"in double quotes becomes?q[]=xin bash/zsh — you'll wrongly conclude "not injectable". Use single quotes /--data-urlencode. $unionWith/$lookupare same-database only. Cross-DB{from:{db,coll}}is a hard MongoDB block ("not supported for db: X") for everything except a couple ofconfig.*system namespaces (empty on standalone). If the app data seems missing, it's almost always a collection-name you haven't guessed in the same DB — not a different DB.$collStats/$listCatalog/$documentscan't nest in$facet(Mongo rejects) — so you can't cheaply list empty collections; rely on$unionWithname-brute for non-empty ones.- Empty
$unionWith/$lookupresult = collection empty or absent; the two look identical.
Verify success
Step 3/4 returns documents whose fields differ from the base collection (e.g. a
token/password/session field where the endpoint should only ever return metadata).
That is data exfiltration from a collection the endpoint never intended to expose.
Learned on
AEGIS lab, 2026-08 — GET /api/v1/aegis-mds/search?pipeline= on aegis_mds.mds_entries;
$facet→$unionWith pending_invites leaked an unredeemed 64-hex WebAuthn invite token →
register/begin (attestation:"none") → software authenticator → login. Full worked example
in aegis/notes.md (§4–6).
Signals
- GitHub stars
- 20
- Forks
- 7
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
api-mongo-agg-facet-bypass- Source
- github.com/noorqureshi/sploitagent
github.com/noorqureshi/sploitagent
Related picks
Skill · wshobson
The pick for Pythonpython-pro
Skill · jeffallan
The pick for Pythonowasp-security
Skill · davila7
The pick for Web (OWASP)owasp-web
Skill · nahid-sparktales
The pick for Web (OWASP)nodejs-backend-patterns
Skill · wshobson
The pick for Noderun-node-tests
Skill · hiroro-work
The pick for Node