GHSA-WR5M-5JPH-8M96
Vulnerability from github – Published: 2026-11-10 20:57 – Updated: 2026-11-10 20:57Summary
Mongoose chooses which discriminator model or schema to use by looking up the discriminator key value (__t by default) in a plain object of registered discriminators, for example Model.discriminators[doc.__t]. That value comes from user documents and query filters. Because the lookup did not check own properties, a value that names a property inherited from Object.prototype resolves to that inherited value instead of a discriminator. Examples are 'constructor', 'toString' and 'hasOwnProperty'.
__t: 'constructor'resolves to the globalObjectfunction. Mongoose then callsnew Object(doc), which returns the raw user object where a Mongoose document is expected.__t: 'toString'and similar names resolve to built-in functions that throw when used as a constructor or schema.
The most serious effect: through bulkWrite() insertOne with skipValidation: true, the raw user object is stored as-is, without schema casting or strict filtering. After that, every query that returns the stored document throws, until the document is removed from the database.
Details
The registries that hold discriminators are plain objects: Model.discriminators, schema.discriminators, and the embedded discriminator registries on document arrays and single nested subdocuments. Several code paths indexed them directly with a value taken from the document or filter:
| Code path | Input |
|---|---|
Model constructor (new Model(doc)) |
document |
Model.create(), insertMany(), bulkWrite(), bulkSave() |
documents |
Hydrating query results (find(), findOne(), etc.) and Model.hydrate() |
stored documents |
Casting query filters, including flat paths such as 'events.__t' and $elemMatch |
query filter |
| Casting updates and array filters | update filter, arrayFilters |
Document array push() / create(), document array and single nested casting |
subdocuments |
| Populate on embedded discriminators | stored subdocuments |
getDiscriminatorByValue() already iterated the registry's own keys safely. But most callers tried the direct lookup first and only fell back to it afterwards, so a truthy inherited value took precedence.
The lookups only read inherited properties. They do not write to Object.prototype or any other shared object, and they do not change in-memory state that affects other requests. This is NOT a prototype pollution vector.
Proof of concept
const mongoose = require('mongoose');
const Base = mongoose.model('Base', new mongoose.Schema({ name: String, age: Number }));
Base.discriminator('Child', new mongoose.Schema({ extra: String }));
// Type confusion: the "document" is the raw input object
const doc = new Base({ name: 'x', __t: 'constructor' });
console.log(doc instanceof mongoose.Document); // false
// Schema bypass and persistent poison document via bulkWrite + skipValidation
await Base.bulkWrite([{
insertOne: { document: { name: 'x', age: 'not a number', notInSchema: true, __t: 'constructor' } }
}], { skipValidation: true });
// Stored verbatim: { age: 'not a number', notInSchema: true, __t: 'constructor', ... }
await Base.find(); // throws: Cannot read properties of undefined (reading 'eachPath')
Impact
- Crashing the request. An attacker who controls the discriminator key of a document or filter can make the request fail. In most code paths that sent the value, this means a
TypeErroror a non-document object returned to application code. Ways in includeModel.create(req.body),new Model(req.body),Model.find(req.query)-style filters, and pushing user input onto embedded discriminator arrays. The effect is limited to the request carrying the value. - Schema bypass on
bulkWrite({ skipValidation: true }). When an application passes untrusted documents tobulkWrite()insertOnewithskipValidation: true, the raw object is stored without casting orstrictfiltering. Fields the schema would normally strip are persisted, such as anisAdminthat isn't in the schema. Fields with wrong types are also persisted. - Persistent denial of service for queries. A stored document with such a discriminator key value makes hydration throw. Every query that returns that document fails, for all users, until the document is deleted directly in the database. Besides the
bulkWrite()path above, the document can get into the collection through another application, the raw MongoDB driver, or any other writer that does not go through Mongoose casting.
This issue doesn't let an attacker modify Object.prototype, built-in functions, schemas or other shared in-memory state. The schema bypass requires the application to opt in to skipValidation with untrusted input.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "mongoose"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "6.13.12"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "mongoose"
},
"ranges": [
{
"events": [
{
"introduced": "7.0.0"
},
{
"fixed": "7.8.13"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "mongoose"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.24.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "mongoose"
},
"ranges": [
{
"events": [
{
"introduced": "9.0.0"
},
{
"fixed": "9.11.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-125116"
],
"database_specific": {
"cwe_ids": [
"CWE-843"
],
"github_reviewed": true,
"github_reviewed_at": "2026-11-10T20:57:18Z",
"nvd_published_at": "2026-11-09T23:17:21Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nMongoose chooses which discriminator model or schema to use by looking up the discriminator key value (`__t` by default) in a plain object of registered discriminators, for example `Model.discriminators[doc.__t]`. That value comes from user documents and query filters. Because the lookup did not check own properties, a value that names a property inherited from `Object.prototype` resolves to that inherited value instead of a discriminator. Examples are `\u0027constructor\u0027`, `\u0027toString\u0027` and `\u0027hasOwnProperty\u0027`.\n\n- `__t: \u0027constructor\u0027` resolves to the global `Object` function. Mongoose then calls `new Object(doc)`, which returns the raw user object where a Mongoose document is expected.\n- `__t: \u0027toString\u0027` and similar names resolve to built-in functions that throw when used as a constructor or schema.\n\nThe most serious effect: through `bulkWrite()` `insertOne` with `skipValidation: true`, the raw user object is stored as-is, without schema casting or `strict` filtering. After that, every query that returns the stored document throws, until the document is removed from the database.\n\n### Details\n\nThe registries that hold discriminators are plain objects: `Model.discriminators`, `schema.discriminators`, and the embedded discriminator registries on document arrays and single nested subdocuments. Several code paths indexed them directly with a value taken from the document or filter:\n\n| Code path | Input |\n|---|---|\n| Model constructor (`new Model(doc)`) | document |\n| `Model.create()`, `insertMany()`, `bulkWrite()`, `bulkSave()` | documents |\n| Hydrating query results (`find()`, `findOne()`, etc.) and `Model.hydrate()` | stored documents |\n| Casting query filters, including flat paths such as `\u0027events.__t\u0027` and `$elemMatch` | query filter |\n| Casting updates and array filters | update filter, `arrayFilters` |\n| Document array `push()` / `create()`, document array and single nested casting | subdocuments |\n| Populate on embedded discriminators | stored subdocuments |\n\n`getDiscriminatorByValue()` already iterated the registry\u0027s own keys safely. But most callers tried the direct lookup first and only fell back to it afterwards, so a truthy inherited value took precedence.\n\nThe lookups only read inherited properties. They do not write to `Object.prototype` or any other shared object, and they do not change in-memory state that affects other requests. This is NOT a prototype pollution vector.\n\n### Proof of concept\n\n```js\nconst mongoose = require(\u0027mongoose\u0027);\n\nconst Base = mongoose.model(\u0027Base\u0027, new mongoose.Schema({ name: String, age: Number }));\nBase.discriminator(\u0027Child\u0027, new mongoose.Schema({ extra: String }));\n\n// Type confusion: the \"document\" is the raw input object\nconst doc = new Base({ name: \u0027x\u0027, __t: \u0027constructor\u0027 });\nconsole.log(doc instanceof mongoose.Document); // false\n```\n\n```js\n// Schema bypass and persistent poison document via bulkWrite + skipValidation\nawait Base.bulkWrite([{\n insertOne: { document: { name: \u0027x\u0027, age: \u0027not a number\u0027, notInSchema: true, __t: \u0027constructor\u0027 } }\n}], { skipValidation: true });\n// Stored verbatim: { age: \u0027not a number\u0027, notInSchema: true, __t: \u0027constructor\u0027, ... }\n\nawait Base.find(); // throws: Cannot read properties of undefined (reading \u0027eachPath\u0027)\n```\n\n### Impact\n\n- **Crashing the request.** An attacker who controls the discriminator key of a document or filter can make the request fail. In most code paths that sent the value, this means a `TypeError` or a non-document object returned to application code. Ways in include `Model.create(req.body)`, `new Model(req.body)`, `Model.find(req.query)`-style filters, and pushing user input onto embedded discriminator arrays. The effect is limited to the request carrying the value.\n- **Schema bypass on `bulkWrite({ skipValidation: true })`.** When an application passes untrusted documents to `bulkWrite()` `insertOne` with `skipValidation: true`, the raw object is stored without casting or `strict` filtering. Fields the schema would normally strip are persisted, such as an `isAdmin` that isn\u0027t in the schema. Fields with wrong types are also persisted.\n- **Persistent denial of service for queries.** A stored document with such a discriminator key value makes hydration throw. Every query that returns that document fails, for all users, until the document is deleted directly in the database. Besides the `bulkWrite()` path above, the document can get into the collection through another application, the raw MongoDB driver, or any other writer that does not go through Mongoose casting.\n\nThis issue doesn\u0027t let an attacker modify `Object.prototype`, built-in functions, schemas or other shared in-memory state. The schema bypass requires the application to opt in to `skipValidation` with untrusted input.",
"id": "GHSA-wr5m-5jph-8m96",
"modified": "2026-11-10T20:57:18Z",
"published": "2026-11-10T20:57:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/security/advisories/GHSA-wr5m-5jph-8m96"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-125116"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/commit/0ecd8040d9b446f36535d79938c130c800f91766"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/commit/18afab75bea861f625b7c5b54635c84567ec566c"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/commit/390453c8ce1b36b6cfd1e63432a910da1f3d81d5"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/commit/8cee51beead554df1f3d05645edb439c69eb2a9b"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/commit/b26c43554233134ea978c5852babe13be858b2cf"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/commit/b68f90734b50cd22c89a669c8ca1469d7234b296"
},
{
"type": "PACKAGE",
"url": "https://github.com/Automattic/mongoose"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/releases/tag/6.13.12"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/releases/tag/7.8.13"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/releases/tag/8.24.7"
},
{
"type": "WEB",
"url": "https://github.com/Automattic/mongoose/releases/tag/9.11.4"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "Mongoose: Discriminator key values matching inherited properties (e.g. __t: \u0027constructor\u0027) bypass schema casting and can poison collections"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.