Neutron, our AI engine, scored 97.1% on UC Berkeley's CyberGym benchmark. Learn more

Deep Dive

Deep Dive

Mongoose NoSQL Injection Bypass, Fixed in 9.11.4 (CVE-2026-125120)

Mongoose applies sanitizeFilter unevenly, so attacker-controlled filters reach countDocuments and connection-level queries; fixed in 9.11.4.

TL;DR

  • Mongoose's sanitizeFilter defense is not applied on countDocuments, is ignored when set on a connection, and three code paths copy inherited properties. Attacker-controlled filters can bypass authorization, alter counts or crash queries.
  • Mongoose before 6.13.12, 7.8.13, 8.24.7 and 9.11.4 is affected across the four supported lines. The countDocuments flaw is CVE-2026-125120, rated CVSS v3.1 9.8 Critical.
  • No exploitation in the wild is known. The advisories include proof-of-concept code, and none of the flaws is in CISA's Known Exploited Vulnerabilities catalog as of November 11, 2026.
  • Upgrade to 6.13.12, 7.8.13, 8.24.7 or 9.11.4. If you rely on sanitizeFilter, add the countDocuments middleware from the advisory until every service is updated. See "How do I fix it?".

Am I affected?

All of these must hold:

  • Your application uses Mongoose and passes attacker-controlled values into query filters, updates or document construction.
  • The application relies on sanitizeFilter for protection. That can be a global mongoose.set('sanitizeFilter', true), a connection option, or a per-query option.
  • For the critical flaw, the application calls countDocuments with a filter that contains attacker-controlled values.

The other two advisories widen the conditions. The connection-level flaw affects applications that set sanitizeFilter on a connection created with mongoose.createConnection(uri, { sanitizeFilter: true }). The inherited-property flaw affects applications that load documents or set dotted paths with attacker-controlled field names, even without sanitizeFilter.

Mongoose is a widely used MongoDB library for Node.js. The npm registry recorded 8,868,838 downloads of the mongoose package in the week ending November 9, 2026, and the GitHub repository has 27,449 stars.

Branch Affected versions Fixed version
9.x 9.0.0 to 9.11.3 9.11.4, released 2026-10-23
8.x 8.0.0 to 8.24.6 8.24.7, released 2026-10-23
7.x 7.0.0 to 7.8.12 7.8.13
6.x 6.0.0 to 6.13.11 6.13.12

The table uses the highest fix floor across the three advisories. The countDocuments flaw alone was fixed in 9.9.2, 8.24.3, 7.8.12 and 6.13.11 on August 10, 2026. The connection-level and inherited-property fixes shipped later, in 9.11.4, 8.24.7, 7.8.13 and 6.13.12. Installing only the August versions leaves the two High flaws open.

Check the installed version:

npm ls mongoose
grep -A2 '"node_modules/mongoose"' package-lock.json
yarn why mongoose
pnpm list mongoose

Then find where the defense is configured:

grep -R "sanitizeFilter" --include='*.js' --include='*.ts' .

A connection created with { sanitizeFilter: true } is exposed to CVE-2026-125119 even though connection.get('sanitizeFilter') returns true.

How do I fix it?

Upgrade to the fixed version for your major line and redeploy every service that uses Mongoose.

  1. On Mongoose 9.x, install 9.11.4 or later: npm install mongoose@9.11.4
  2. On Mongoose 8.x, install 8.24.7 or later: npm install mongoose@8.24.7
  3. On Mongoose 7.x, install 7.8.13 or later: npm install mongoose@7.8.13
  4. On Mongoose 6.x, install 6.13.12 or later: npm install mongoose@6.13.12
  5. Rebuild and redeploy each service, then run your authentication and count-based authorization tests against the new version.

If you cannot upgrade immediately, the countDocuments advisory publishes a middleware workaround. It applies sanitizeFilter before the count runs:

mongoose.plugin(function() {
  schema.pre('countDocuments', function() {
    // This will always apply `sanitizeFilter` on `countDocuments`
    mongoose.sanitizeFilter(this.getFilter());
  });
});

For the connection-level flaw, do not rely on createConnection(uri, { sanitizeFilter: true }). Set the option globally with mongoose.set('sanitizeFilter', true) or per query with .sanitizeFilter(). That workaround does not fix the stored inherited-property flaw; only the upgrade does. Validate the field names your application accepts if you cannot upgrade, and reject paths such as toString, valueOf, constructor and $__schema.

What is the root cause?

Mongoose's sanitizeFilter wraps operator objects in filter values in $eq, so a value such as { $ne: null } is matched as a literal instead of being interpreted as a MongoDB operator. The defense is only effective when every query path applies it. These three advisories are three gaps in that promise.

The first gap is countDocuments. In 8.24.2, _countDocuments casts the conditions directly instead of going through _castConditions, which is the function that applies sanitizeFilter:

Query.prototype._countDocuments = async function _countDocuments() {
  this._applyTranslateAliases();

  try {
    this.cast(this.model);
  } catch (err) {
    this.error(err);
  }

_castConditions checks the configured options and calls sanitizeFilter(this._conditions), but countDocuments never reaches it. The fix changes the method to call _castConditions().

The second gap is the connection-level option. NativeConnection#createClient() moves sanitizeFilter from the connection options into the connection's config object and deletes it from options:

    if ('sanitizeFilter' in options) {
      this.config.sanitizeFilter = options.sanitizeFilter;
      delete options.sanitizeFilter;
    }

Query.prototype._castConditions at 8.24.6 reads this.model?.db.options?.sanitizeFilter, then this.model?.base.options?.sanitizeFilter, then the per-query option. It never reads this.model.db.config.sanitizeFilter, so the connection-level option is silently ignored even though connection.get('sanitizeFilter') returns true.

The third gap is inherited properties. Mongoose blocks __proto__, constructor and prototype, but its document hydration and path-setting code does not check whether a property is the object's own. In init(), a stored field named toString resolves to the inherited Object.prototype.toString, which is truthy, so Mongoose recurses into the function and copies the stored subfields onto it:

      // assume nested object
      if (!doc[i]) {
        doc[i] = {};
        if (!strict && !(i in docSchema.tree) && !(i in docSchema.methods) && !(i in docSchema.virtuals)) {
          self[i] = doc[i];
        } else if (opts?.virtuals && (i in docSchema.virtuals)) {
          self[i] = doc[i];
        }
      }
      init(self, value, doc[i], opts, path + '.');

The same own-property check is missing in $__set, which handles dotted paths, and in utils.mergeClone, which merges update documents. The fix adds Object.hasOwn checks before writing.

How is it exploited?

  1. For CVE-2026-125120, the application calls Model.countDocuments({ username: req.body.username, password: req.body.password }) while sanitizeFilter is enabled. The attacker sends {"password": {"$ne": null}}.
  2. Because countDocuments skipped _castConditions, the operator object reaches MongoDB as an operator. The count returns the matching account even when the password is wrong.
  3. An authentication or quota check that treats a non-zero count as success grants access or returns data the attacker should not see.
  4. For CVE-2026-125119, the application creates a connection with sanitizeFilter: true and passes {"password": {"$ne": null}} into findOne. The option is in Connection.config, not Connection.options, so the filter is not sanitized and the query returns the first account.
  5. For CVE-2026-125117, the attacker stores or submits a field named toString with a value such as {"call": 1}. Hydration copies the value onto the shared function, and every later query in the process throws TypeError: _toString.call is not a function.
  6. The same gap can reach a subdocument's shared schema, for example by setting profile.$__schema.paths.email.validators to an empty array, which removes the required validator for every document in the process.

The advisories include proof-of-concept code for the connection-level and inherited-property flaws. The critical advisory includes a test that shows the operator being wrapped in $eq after the fix. No public exploitation in the wild is reported.

How does the attack flow?

An attacker-controlled operator object reaches a query path that skipped the sanitizer, so MongoDB interprets it as an operator and returns data or changes state the application did not intend.

flowchart LR A["Attacker sends a filter value
such as { $ne: null }"] --> B["Application passes it to
countDocuments or a connection query"] B --> C["sanitizeFilter is skipped
or stored in the wrong option"] C --> D["MongoDB treats the value
as an operator"] D --> E["Query returns a record
or a count the app trusts"] E --> F["Authentication bypass
or data exposure"]

What can an attacker do?

The impact depends on the path. The countDocuments flaw lets an attacker change the result of a count that the application uses for authorization, pagination, quotas or business logic. The connection-level flaw lets an attacker bypass a password check and read an account record. The inherited-property flaw lets an attacker overwrite shared built-in functions and cause a denial of service, or alter shared schema validators.

The conditions are:

  • The application passes attacker-controlled objects into Mongoose filters, updates or document construction.
  • For the critical flaw, the application uses countDocuments and relies on sanitizeFilter.
  • For the connection-level flaw, the application sets sanitizeFilter on createConnection rather than globally or per query.
  • For the inherited-property flaw, the application accepts field names or dotted paths from an attacker, or stores documents whose field names an attacker controls.

The advisories describe data disclosure and denial of service. They do not describe remote code execution, and no source demonstrates code execution through these paths.

Why does the fix work?

The fixes close each gap at the point where the filter is prepared. For countDocuments, the method now calls _castConditions, so sanitizeFilter runs before the query reaches MongoDB:

 Query.prototype._countDocuments = async function _countDocuments() {
   this._applyTranslateAliases();
-
-  try {
-    this.cast(this.model);
-  } catch (err) {
-    this.error(err);
-  }
+  this._castConditions();

   if (this.error()) {
     throw this.error();

For the connection-level option, _castConditions now reads the connection's config first:

 Query.prototype._castConditions = function() {
-  let sanitizeFilterOpt = undefined;
-  if (this.model?.db.options?.sanitizeFilter != null) {
-    sanitizeFilterOpt = this.model.db.options.sanitizeFilter;
-  } else if (this.model?.base.options?.sanitizeFilter != null) {
-    sanitizeFilterOpt = this.model.base.options.sanitizeFilter;
-  } else {
-    sanitizeFilterOpt = this._mongooseOptions.sanitizeFilter;
-  }
+  const sanitizeFilterOpt = this.model?.db.config?.sanitizeFilter ??
+    this.model?.db.options?.sanitizeFilter ??
+    this.model?.base.options.sanitizeFilter ??
+    this._mongooseOptions.sanitizeFilter;

For inherited properties, the document code checks Object.hasOwn before it writes:

       // assume nested object
+      // Skip inherited properties like `Object.prototype.toString`
+      // to avoid setting properties on shared objects
+      if (!Object.hasOwn(doc, i) && i in doc) {
+        continue;
+      }
       if (!doc[i]) {
         doc[i] = {};

The same check lands in the dotted-path setter and the merge helper. A stored toString field is now ignored instead of being copied onto the shared function, and a dotted path that names an inherited property becomes a no-op.

The fixes are complete for the three code paths the advisories describe. Applications still need to avoid passing raw request objects into filters where the schema expects a scalar; sanitizeFilter reduces the risk but does not validate application semantics.

How do I check if I was compromised?

The advisories publish no indicators of compromise and no exploitation in the wild is known. The evidence would be application-level, not a known file or network signature.

Look for queries that carry operator objects where your code expects a scalar. If your application logs query filters, search for $ne, $gt, $where or $expr in request-derived values:

grep -E '\$(ne|gt|gte|lt|lte|where|expr)' /var/log/your-app.log

A hit is a lead, not proof: legitimate code can use those operators. Correlate it with authentication or count-based authorization events from the same source address.

Look for the denial-of-service symptom of the inherited-property flaw in the application log:

grep -E '_toString\.call is not a function|_toString\.call is not' /var/log/your-app.log

A process that starts throwing that error on every query after a document was loaded or updated is consistent with the flaw. Restarting the process clears the symptom, but the stored document remains, so find and clean the record.

Check whether collections contain documents with field names that shadow built-ins. This query returns a lead, not a verdict, because a legitimate schema can use those names:

db.getCollectionNames().forEach(function(name) {
  const hit = db[name].findOne({ $or: [{ toString: { $exists: true } }, { valueOf: { $exists: true } }] });
  if (hit) { print(name); }
});

If you find a document that was written by an attacker, remove it, rotate any credential the application process could read, and review the records the affected queries returned.

Timeline

  • 2026-08-09: the countDocuments fix is committed.
  • 2026-08-10: Mongoose 6.13.11, 7.8.12, 8.24.3 and 9.9.2 ship the countDocuments fix.
  • 2026-10-22: the connection-level and inherited-property fixes are committed.
  • 2026-10-23: Mongoose 8.24.7 and 9.11.4 ship the second set of fixes.
  • 2026-11-09: the CVE records for CVE-2026-125120, CVE-2026-125119 and CVE-2026-125117 are published.
  • 2026-11-10: the GitHub advisories are published.

Timeline of the August and October fixes and the November advisories
Figure 1: The fixes shipped in August and October, while the CVE records and GitHub advisories appeared in November. Source: Mongoose releases and advisories, checked November 11, 2026

FAQ

Does this affect every Mongoose application?

No. The critical flaw requires the application to call countDocuments with attacker-controlled filter values while relying on sanitizeFilter. The connection-level flaw requires the option to be set on createConnection. The inherited-property flaw is broader: any application that loads documents or sets dotted paths with attacker-controlled field names can reach it. Updating is the safe default for all three.

Is there a public proof of concept?

Yes for the connection-level and inherited-property flaws. Both advisories include code that demonstrates the bypass, and CISA's SSVC block for CVE-2026-125117 records Exploitation: poc. The countDocuments advisory includes a regression test rather than a standalone exploit.

Is it exploited in the wild?

No exploitation in the wild is known as of November 11, 2026, and none of the three CVEs is in CISA's Known Exploited Vulnerabilities catalog. The public proof-of-concept code means the window between disclosure and attempts is short, so update on the normal schedule rather than waiting for a report.

Which version is the safe floor?

Use 6.13.12, 7.8.13, 8.24.7 or 9.11.4. The August releases fix only the critical countDocuments flaw; the October releases add the connection-level and inherited-property fixes. Installing 8.24.3 or 9.9.2 leaves two High flaws open.

Why did the advisories appear months after the fixes?

The fixes shipped in August and October 2026, but the GitHub advisories and CVE records were published on November 9 and 10, 2026. That gap is common for repository advisories that are reviewed later. The dates above come from the release tags and the CVE records, and the post uses the fix dates, not the advisory dates, to describe the patch history.

Do I need to update if I do not use sanitizeFilter?

Yes, because the inherited-property flaw does not depend on sanitizeFilter. It depends on attacker-controlled field names reaching Mongoose's document hydration, dotted-path setter or merge code. If your schema accepts arbitrary keys, the flaw applies even when the filter defense is off.

Find vulnerable Mongoose versions with Ostorlab

Ostorlab's software composition analysis and SBOM inventories the open-source dependencies in your repositories and container images, so you can see which services still resolve mongoose to an affected release and track the upgrade to 6.13.12, 7.8.13, 8.24.7 or 9.11.4. Run it across the repositories that build your Node.js services and use the results to prioritize the ones that pass request data into query filters.

References

  • Mongoose advisory GHSA-pcrw-h46h-c93h (CVE-2026-125120): https://github.com/Automattic/mongoose/security/advisories/GHSA-pcrw-h46h-c93h (checked November 11, 2026)
  • Mongoose advisory GHSA-gg4r-jv3p-qx3c (CVE-2026-125119): https://github.com/Automattic/mongoose/security/advisories/GHSA-gg4r-jv3p-qx3c (checked November 11, 2026)
  • Mongoose advisory GHSA-8332-j7wj-wjw4 (CVE-2026-125117): https://github.com/Automattic/mongoose/security/advisories/GHSA-8332-j7wj-wjw4 (checked November 11, 2026)
  • Fix commit for countDocuments (7e912af3): https://github.com/Automattic/mongoose/commit/7e912af3fac52cb0eea4cd71e24d65318916831a (checked November 11, 2026)
  • Fix commit for the connection-level option (4bb65947): https://github.com/Automattic/mongoose/commit/4bb659478be2f30655a7bdd78bc251bdf111cd29 (checked November 11, 2026)
  • Fix commit for inherited properties (18b5513d): https://github.com/Automattic/mongoose/commit/18b5513dd463731a2caf0d3395ef1d9eb0735f50 (checked November 11, 2026)
  • Mongoose 9.11.4 release: https://github.com/Automattic/mongoose/releases/tag/9.11.4 (checked November 11, 2026)
  • Mongoose 8.24.7 release: https://github.com/Automattic/mongoose/releases/tag/8.24.7 (checked November 11, 2026)
  • npm download count for mongoose: https://npm-stat.com/charts.html?package=mongoose&from=2026-11-03&to=2026-11-09 (checked November 11, 2026)
  • CVE-2026-125120 record: https://www.cve.org/CVERecord?id=CVE-2026-125120 (checked November 11, 2026)
  • CVE-2026-125117 record: https://www.cve.org/CVERecord?id=CVE-2026-125117 (checked November 11, 2026)