| name | api-mongo-agg-facet-bypass |
| description | 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.
|
| domain | api |
| type | technique |
| stability | learning |
| modes | ["pentest","bugbounty"] |
| severity | high |
| owasp | ["A03:2021-Injection"] |
| cwe | ["CWE-943"] |
| schema_version | 1 |
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
pipeline param 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.
$facet containing 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"
here is legal ONLY inside (top-level → allowlist rejects it).