DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

NoSQL Injection in APIs: From Auth Bypass to JavaScript Execution via MongoDB Operators

NoSQL Injection in APIs: From Auth Bypass to JavaScript Execution via MongoDB Operators

Every NoSQL injection tutorial shows the same payload: {"$gt": ""}. You log in with {"password": {"$ne": null}} and the database returns the first document satisfying the modified query. The impact is real, but bounded by what the query layer can do.

What no guide maps is what happens when the injected operator reaches a JavaScript execution context. $ne filters documents. $where executes code inside the database process. The impact difference between these two classes is the difference between auth bypass and RCE.

Three Operator Families, Three Impact Classes

MongoDB separates operators by function in the query pipeline. The first family, $ne, $gt, $in, $regex, operates at the query layer. These operators filter documents based on value comparisons. When an attacker injects {"password": {"$ne": null}}, MongoDB returns documents where the password field holds any value other than null. Impact: auth bypass or data exfiltration via blind oracle.

The second family, $where and $function, operates in a different context. $where accepts a JavaScript string and executes it as a query predicate inside the MozJS engine embedded in the mongod process. $function does the same in aggregation pipelines, available since MongoDB 4.4. The impact is not returning the wrong documents; it is executing arbitrary code inside the database process.

The third surface does not exist in MongoDB directly. When Mongoose uses the sift library to filter populate() results, operators like $where execute in the application's Node.js process, not in MongoDB. This invalidates the --noscripting flag, which only applies to MongoDB's MozJS engine. No existing guide distinguishes these three layers or explains why the operator family determines the achievable impact class.

$regex as a Blind Oracle: H1 #1130721 and CVE-2021-22911

$regex does not execute JavaScript. It filters documents by pattern match. The impact depends entirely on what data can be extracted through differential application responses.

The Rocket.Chat case documents the real impact of the $regex class with precision. CVE-2021-22911 (CVSS 9.8, H1 #1130721): the getPasswordPolicy method accepted the token parameter without validation. An attacker sent POST /api/v1/users.forgotPassword with body {"email": {"$regex": "^a"}}. If a user with an email starting with "a" existed, the response differed from the response for a nonexistent email. With binary search across the 62-character alphabet, any password reset token could be extracted without authentication.

The impact chain: token extracted via oracle, admin password reset, admin cookie, webhook configured with arbitrary command, RCE. Over 40 public exploits document production exploitation across Rocket.Chat versions 3.11 to 3.13.

CVE-2024-37405 (H1 #2580062, disclosed 2024-07-12) demonstrated that the $regex impact class survived the 2021 patch. Two Rocket.Chat livechat methods, livechat:loginByToken and livechat:loadHistory, accepted MongoDB operators in their parameters. $regex chained across two methods allowed extracting visitor messages without authentication. H1 #1458020 confirms the same class: $regex in the fileId parameter of the getS3FileUrl method exposed arbitrary S3 upload URLs in versions prior to 5.0.

The confirmation probe is direct: replace a string value with {"field": {"$regex": "."}} and compare the response against a literal value. A different response confirms MongoDB operator acceptance at that parameter.

$where in the Mongoose Sift Layer: Execution on the Application Server

The --noscripting flag on mongod disables JavaScript execution in the database process. It is a standard hardening recommendation that appears on every MongoDB security checklist. CVE-2024-53900 demonstrated that this control does not protect what most engineers assume it protects.

Mongoose prior to 8.8.3 (CVSS 9.1): when Model.find().populate({match: userInput}) is called, Mongoose uses the sift library to filter populated documents in memory. Sift is a JavaScript implementation of MongoDB query operators, running in the application's Node.js process, not in MongoDB's MozJS engine. A payload {"$where": "process.exit(1)"} passed via the populate() match executes on the application server. --noscripting has no effect because the restriction applies to the database engine, not to the Node.js runtime.

{
  "filter": {
    "$where": "require('child_process').execSync('id > /tmp/pwned')"
  }
}
Enter fullscreen mode Exit fullscreen mode

The Mongoose 8.8.3 fix blocked $where at the top level of the match object. CVE-2025-23061 (Mongoose prior to 8.9.5) documented the bypass: {"$or": [{"$where": "process.exit(1)"}]}. $or is a valid operator in both contexts, MongoDB and sift. The 8.8.3 validator checked only top-level properties; one level of nesting was enough to bypass the patch with identical impact.

H1 #1130874 illustrates $where in an authenticated API context. Rocket.Chat's users.list API accepted MongoDB filters containing $where. An authenticated attacker extracted admin credentials and 2FA secrets, then used an admin webhook for command execution via console.log.constructor.constructor('return process')().mainModule.require('child_process').execSync. The definitive fix is in Mongoose 8.9.5, which blocks $where and $function across the entire populate() match object, including arbitrary nesting.

How to Distinguish Auth Bypass from JS Execution During the Probe Phase

The probe methodology is sequential. Each step confirms an impact class before attempting the next.

Step 1, query layer: send {"param": {"$ne": null}} in authentication fields. A 200 response on a login that should fail confirms operators reach the query layer. Impact class: auth bypass or data oracle.

Step 2, blind oracle: send {"param": {"$regex": "^a"}} and compare against {"param": {"$regex": "^z"}}. Differential responses for distinct patterns confirm a blind oracle is available. Impact class: data exfiltration via binary search.

Step 3, JavaScript execution: send {"param": {"$where": "sleep(5000)||1"}} and measure latency against baseline. A response with a 5-second delay confirms JavaScript execution. The layer executing it, MongoDB engine or Node.js sift, determines whether --noscripting has any effect.

Step 4, nesting bypass for Mongoose stacks: test {"$or": [{"$where": "sleep(5000)||1"}]} on endpoints that use populate(). This payload targets the CVE-2025-23061 pattern in versions prior to 8.9.5. Verify the Mongoose version through observable artifacts such as the X-Powered-By header, exposed error messages, or an accessible package.json.

Probe: Which Parameters Accept MongoDB Operators

The target is any parameter in a JSON body that filters or queries data: login fields, search fields, user ID parameters, and filter objects in listing endpoints. The test payload replaces a string value with {"$ne": null} and observes whether the response changes.

POST /api/auth/login HTTP/1.1
Content-Type: application/json

{"username": {"$ne": null}, "password": {"$ne": null}}
Enter fullscreen mode Exit fullscreen mode

Endpoints performing relational lookups frequently use populate() with user-controlled match objects. Comments, tags, related posts, and populated user fields are the direct vectors for CVE-2024-53900 and CVE-2025-23061.

POST /api/posts/search HTTP/1.1
Content-Type: application/json

{"filter": {"$or": [{"$where": "sleep(3000)||1"}]}}
Enter fullscreen mode Exit fullscreen mode

The tech_detector from (MAGO team tool) identifies APIs with MongoDB backends through fingerprints in error messages ("MongoError", "duplicate key"), timing patterns in blind tests with $regex vs $gt, and Mongoose version signatures in observable artifacts.

The difference between $ne and $where is not technical; it is the execution layer. $ne filters documents. $where executes code. Mongoose 8.8.3 made exactly that confusion in the sift layer, and CVE-2025-23061 exploited exactly that.

Top comments (0)