Threats tagged 'ghsa-8332-j7wj-wjw4'
1 threat
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| High | 8.2 | EPSS 0.6% | CVE-2026-125117Mongoose walks into inherited properties when hydrating, setting, and merging, allowing overwrites of built-in High8.2EPSS 0.6%GCVE DatabaseCVE-2026-125117 ### 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 = | GCVE Database |
Page 1 of 1