noorqureshi/security-skills/api-mongo-agg-facet-bypass
Claude Code and agent skill by noorqureshi/security-skills
- community
Checking that the SKILL.md file is still in the repository.
Bypass 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.
Install
npx vskill@latest install noorqureshi/security-skills/api-mongo-agg-facet-bypassSKILL.md
MongoDB aggregation injection — $facet allowlist bypass → cross-collection read
When it applies
- An API runs a user-controlled aggregation pipeline on a fixed collection
(db.collection('x').aggregate(userPipeline)), usually exposed as a pipeline query/body parameter for "advanced" search.
- The server defends with a stage allowlist — only screens the top-level stage
names ($match/$project/$sort/$limit/$facet allowed; $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):
``json [{"$facet":{"r":[ {"$match":{"<anyfield>":"__none__"}}, {"$unionWith":{"coll":"<target_collection>","pipeline":[{"$limit":20}]}} ]}}] ` $unionWith here 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. known mds_entries ⇒ try pending_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 Python urllib helper that JSON-encodes and URL-encodes the pipeline (cleaner for building big $facet maps and parsing results). See aegis/exploit-dev/*.py for 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`/`$lookup` are same-database only. Cross-DB
{from:{db,coll}}is a hard
MongoDB block ("not supported for db: X") for everything except a couple of config.* 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 $unionWith name-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).
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/api/api-mongo-agg-facet-bypass/SKILL.md
Popularity
- 7-day trend score
- 0
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/noorqureshi/security-skills/api-mongo-agg-facet-bypass)