Mongoose walks into inherited properties when hydrating, setting, and merging, allowing overwrites of built-in
Affects mongooseSource GCVE Database
Not known to be exploited · CVSS 8.2 High : upgrade to 6.13.12 soon.
Data as of 10 Nov 2026, 23:24 UTC
Thin evidence
One scorer or none| CNA (GitHub_M) | CVSS 3.1 | 8.2 |
|---|
- EPSS
- 0.5% chance of exploitation in 30 days – higher than 48% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
- Patch
- Available – fixed in 6.13.12, 7.8.13, 8.24.7 +1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:HExploitation evidence
Disclosed: evidence found.
CVE published 9 Nov 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 10 Nov 2026.
What to do
Upgrade to a fixed release listed under Affected software.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
| npmmongoose | Affected
| Fixed in
|
| npmmongoose | Affected
| Fixed in
|
| npmmongoose | Affected
| Fixed in
|
| npmmongoose | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
### Summary Three Mongoose code paths copy data into objects by property name without checking whether the property is the object's own or inherited from its prototype: 1. Loading a document from the database (`init()`, also used by `Model.hydrate()`) 2. Setting a dotted path on a document (`$__set`, used by `new Model(obj)`, `doc.set()` and `doc.$set()`) 3. Merging update documents (`utils.mergeClone`, used by `findOneAndReplace()`, chained update calls and `Query.prototype.merge()`) When a key names an inherited property, such as `toString` or `valueOf`, or a document internal such as `$__schema`, Mongoose writes into the shared object behind that property. The data comes from the attacker. - **Denial of service.** For example, `{ "toString": { "call": 1 } }` overwrites `Object.prototype.toString.call`. Mongoose and most other libraries rely on `Object.prototype.toString.call()`. After the overwrite, every later query in the process throws `TypeError: _toString.call is not a function` until the process restarts. This includes queries on unrelated models. - **Changing shared schema state.** Through a subdocument with `strict: false`, a path such as `profile.$__schema.paths.email.validators` reaches the subdocument's schema, which every document shares. Setting it to `[]` removes the `required` validator for every document in the process. Mongoose already blocked `__proto__`, `constructor` and `prototype`, so these bugs cannot add properties to `Object.prototype` itself. They can still overwrite methods and properties of shared built-in functions and of Mongoose's internal objects. ### Details #### 1. Loading documents: `init()` ```js if (!schemaType && utils.isPOJO(value)) { // assume nested object if (!doc[i]) { doc[i] = {}; ... } init(self, value, doc[i], opts, path + '.'); } ``` For a stored field named `toString`, `doc[i]` is the inherited `Object.prototype.toString`. It is truthy, so `init()` recurses into that function and copies the stored subfields onto it. This runs for every document Mongoose loads: `find()`, `findOne()`, cursors, populated documents and `Model.hydrate()`. It runs whatever the `strict` setting, and it applies to top-level and nested paths alike. `lean()` queries are not affected. A malicious document can get into the collection in several ways: - through a model with `strict: false`, for example `new Model(req.body).save()`; - through another application or the raw MongoDB driver; - through any update that writes arbitrary keys. #### 2. Setting dotted paths: `$__set` When it walks a dotted path, `$__set` read `obj[parts[i]]` and descended into whatever it found. That included inherited functions, such as `Object.prototype.toString` on a plain object or `Document.prototype.toObject` on a subdocument. On subdocuments it also included non-function internals that are prototype getters, such as `$__schema`, `schema`, `$__middleware` and `$__schemaTypeOptions`. Those point at objects that every document of the model shares. Attacker-controlled input reaches this path through: - **A `Mixed` path on a default, strict schema:** `new Model({ "mixed.toString.call": 1 })`, or `doc.set({ "mixed.toString.call": 1 })`. Nothing needs to be saved. - **Any root path on a schema with `strict: false`:** for example `doc.set('toString.call', 1)`. - **A subdocument with `strict: false`**, when the application passes a user-controlled path to `doc.set(path, value)`: for example `doc.set('profile.$__schema.paths.email.validators', [])`. #### 3. Merging updates: `utils.mergeClone` ```js if (typeof to[key] === 'undefined') { to[key] = clone(fromObj[key]); } else if (isObject(val)) { mergeClone(to[key], val); // to[key] may be an inherited built-in } ``` `to[key]` was read without an own-property check, so a key such as `toString` recursed into `Object.prototype.toString`. This affects: - **`Model.findOneAndReplace(filter, replacement)`.** It always merges the replacement into the query's update, so a single call with an untrusted replacement is enough. - **Update calls chained onto a query that already has an update.** For example `Model.updateOne(filter, { $set: {...} }).updateOne({}, untrustedUpdate)`, the same pattern with `findOneAndUpdate()`, or middleware that calls `this.updateOne(...)`. - **`Query.prototype.merge()`** with a source query whose update came from untrusted input. ### Proof of concept ```js const mongoose = require('mongoose'); const { Schema } = mongoose; // 2. Setting dotted paths: a Mixed path on a default strict schema, nothing saved const Item = mongoose.model('Item', new Schema({ name: String, meta: {} })); new Item(JSON.parse('{"meta.toString.call": 1}')); // e.g. new Item(req.body) console.log(typeof Object.prototype.toString.call); // 'number' // From here on, every query in the process fails: // TypeError: _toString.call is not a function ``` ```js // 1. Loading documents: a stored field named after an Object.prototype method const Loose =
References
- github.com/Automattic/mongoose/security/advisories/GHSA-8332-j7wj-wjw4 (opens in a new tab)
- nvd.nist.gov/vuln/detail/CVE-2026-125117 (opens in a new tab)
- github.com/Automattic/mongoose/commit/18b5513dd463731a2caf0d3395ef1d9eb0735f50 (opens in a new tab)
- github.com/Automattic/mongoose/commit/2a195d19a4dd240f039a0a226614d5804e8187ee (opens in a new tab)
- github.com/Automattic/mongoose/commit/3cb702b8d079c3e267f0b10f41673a1590f703c8 (opens in a new tab)
- github.com/Automattic/mongoose/commit/85cf897105edfeb5f56be42a18a68704434cac82 (opens in a new tab)
- github.com/Automattic/mongoose/commit/8cee51beead554df1f3d05645edb439c69eb2a9b (opens in a new tab)
- github.com/Automattic/mongoose/commit/8eb74cb4a550cbe0225963b07aebacbd999d4b74 (opens in a new tab)
- github.com/Automattic/mongoose (opens in a new tab)
- github.com/Automattic/mongoose/releases/tag/6.13.12 (opens in a new tab)
- github.com/Automattic/mongoose/releases/tag/7.8.13 (opens in a new tab)
- github.com/Automattic/mongoose/releases/tag/8.24.7 (opens in a new tab)
- github.com/Automattic/mongoose/releases/tag/9.11.4 (opens in a new tab)