Report: HackerOne #3756002 (MongoDB)
Bounty: $3,000
Severity: Remote Code Execution
Affected: MongoDB Compass ≤ 1.49.7 (verified on macOS 26.5)
Credit: Maliq Barnard
One click from code execution
MongoDB Compass is the official GUI for MongoDB. Its single most common action — connect to a server, expand a database in the sidebar, open a shell on a collection — was enough to run arbitrary code on the analyst's machine. The attacker never touches the victim's computer. They only need to control the name of a database the victim can see.
The database name (String(require('child_process')['execSync']('id'))) executes id on the victim's machine the moment they click Open MongoDB Shell.
Root cause
When you open a shell on a collection, Compass pre-seeds the mongosh session so you land in the right database. It builds that first statement by string-interpolating the database name directly into JavaScript that the mongosh worker then evaluates:
// effectively:
initialEvaluate: `use ${dbName}`
The database name is never quoted or escaped. The mongosh worker evaluates initialEvaluate as JavaScript, and that worker has full require() access — child_process, fs, net, everything Node exposes. So if dbName is not a plain identifier but a JavaScript expression, the expression runs.
use (String(require('child_process')['execSync']('id')))
use() ends up being handed the output of the command (a string), which it rejects as an invalid database name — but only after execSync('id') has already executed.
Why the malicious name is a valid database name
The obvious objection is that MongoDB wouldn't allow such a name. It does. MongoDB only forbids these characters in database names: /\."$*<>:|?, the null byte, and spaces. The payload uses none of them:
- parentheses
()— allowed - single quotes
'— allowed (only double quotes are restricted) - square brackets
[]— allowed - letters and underscores — allowed
The payload is 52 characters, within the 63-character limit. mongosh itself refuses to create a database with parentheses in the name — but the MongoDB server accepts it, and any driver (pymongo, the Node driver, etc.) will create it. That is the realistic attack path: the attacker uses a driver, not the shell.
Proof of concept
Stand up a server and plant the malicious database with a driver:
docker run -d --name shell-rce-poc -p 27018:27017 mongo:7
# create_malicious_db.py
from pymongo import MongoClient
c = MongoClient('localhost', 27018)
name = "(String(require('child_process')['execSync']('id')))"
c[name]['trigger'].insert_one({'x': 'open shell on this collection'})
Then, as the victim:
- Open Compass and connect to
mongodb://localhost:27018. - Expand the server in the sidebar. The malicious database appears — truncated to
(String(require('child_process')['ex..., so the payload isn't even visible. - Expand it, click the
triggercollection, and choose Open MongoDB Shell.
The shell auto-evaluates use (String(require('child_process')['execSync']('id'))) and returns:
MongoshInvalidInputError: [COMMON-10001] Invalid database name: uid=501(maliqbarnard) g...
The error text contains the output of id. That is the proof: require('child_process').execSync('id') ran on the victim's machine, String() turned the result into a string, and only then did use() reject it as an invalid name. If require were unavailable the error would say "require is not defined"; instead it echoes uid=501(...), which means the whole chain executed.
Attack scenarios
Rogue server (strongest). An attacker runs a MongoDB server carrying the malicious database and shares the connection string — "connect to my dev server." The victim connects, browses, clicks a shell: RCE.
Shared / multi-tenant cluster (most realistic). An attacker with write access (dbAdmin on a shared Atlas cluster, a team staging environment, a dev cluster) creates the database once. Every Compass user who connects to that cluster afterward — today, next week, next month — sees it in the sidebar and is one click from RCE. The victim is connecting to a server they already trust and use daily.
Persistence. The malicious database name survives in Compass's saved connections and recent history. After disconnecting, the saved connection persists until the user actively removes it; on reconnect the database reappears and the RCE re-triggers on the next shell open.
This is not "unlikely user interaction." Connecting to a server is Compass's primary function, the sidebar is the default view, and Open MongoDB Shell is a first-class action (context-menu item and a header button). No scripting, no batch operations, no special flags — and the payload is hidden by sidebar truncation.
Impact
An attacker who can set a database name on any server a Compass user connects to gets immediate code execution on the analyst's machine on a single click of a standard UI control. The mongosh worker's require() reaches child_process, fs, and net, so the attacker can run arbitrary commands, read and write files, open reverse shells, and exfiltrate local data.
The fix
Quote the database name before interpolating it into the initial statement, the same way collection names are already handled:
// vulnerable:
initialEvaluate: `use ${dbName}`
// fixed:
initialEvaluate: `use ${JSON.stringify(dbName)}`
The same wrapField sanitization Compass already applies to collection names also closes it. As defense in depth, the argument to use should be treated as a literal name rather than an expression to evaluate. MongoDB triaged the report, awarded a $3,000 bounty, and shipped a fix.
Timeline
| Date | Event |
|---|---|
| May 2026 | Reported to MongoDB via HackerOne (#3756002) |
| July 9, 2026 | $3,000 bounty awarded; fix committed |