The Missing Comma Was a 10.0: Mercury CVE-2026-111057, CVE-2026-111058 and CVE-2026-111059

Within sixty minutes on 19 October 2026, NVD published three CVE records against Mercury (mljar/mercury), the Python framework whose pitch is “turn a Jupyter notebook into a beautiful, shareable web app — no callbacks, no frontend, no rewrite.” The scores tell the story in descending order: CVE-2026-111057 at CVSS 3.1 10.0 (unauthenticated remote code execution through the terminal API), CVE-2026-111058 at 9.8 (unauthenticated remote code execution through the kernel API), and CVE-2026-111059 at 6.1 (stored/DOM cross-site scripting in the Markdown widget). All three come from security-advisories@github.com, all three carry GitHub Security Advisories as their primary references, and — this is the part worth sitting with — all three were fixed in August and September. The identifiers arrived 39 to 59 days after the releases that contained them.

Mercury is not a toy: 4,357 stars, 292 forks, Apache-2.0, under development since December 2021. Its whole product is executing notebooks on a server and showing the results to visitors. That makes every one of these three a statement about the same architectural decision: anonymous visitors were kept away from a live Jupyter server by a blocklist, and the blocklist failed three times in three different ways — once by typo, once by design limitation, and once because the vulnerable sink was never in the blocklist’s jurisdiction at all.

The comma: CVE-2026-111057

CVE-2026-111057 (published 2026-10-19T16:17:03.810Z, vulnStatus Deferred, CWE-185, incorrect regular expression, vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) is the kind of defect that makes experienced engineers go quiet, because everyone has shipped it in some form. The blocklist in mercury_app/block_handler.py was defined as:

BLOCKED_PATTERNS = [
    # JupyterLab UI + API
    r"/lab(?!/(?:extensions/|api/settings(?:/|$)|api/translations(?:/|$)))(?:/.*)?"
    r"/api/terminals(?:/.*)?",
    r"/api/shutdown(?:/.*)?",
    r"/files(?:/.*)?",
]

There is no comma at the end of the first pattern. Python’s implicit string-literal concatenation fused the /lab… rule and the /api/terminals rule into a single regex that matches neither path on its own. A direct request to /api/terminals matched none of the three effective rules and reached Jupyter Server’s built-in terminal API — a real OS shell over a WebSocket, with no authentication, no account and no session required. The entire fix, commit 8c46712 (“fix: block unauthenticated terminal access”, 21 August 2026), is one line in block_handler.py, +1/−1, quoted verbatim from the diff:

-    r"/lab(?!/(?:extensions/|api/settings(?:/|$)|api/translations(?:/|$)))(?:/.*)?"
+    r"/lab(?!/(?:extensions/|api/settings(?:/|$)|api/translations(?:/|$)))(?:/.*)?",

One comma, plus a new regression test file asserting that /api/terminals and /api/terminals/1 are blocked while the Lab settings and translation APIs stay reachable. The companion advisory GHSA-f8c7-rwf9-fmgm (published 21 August 2026, affected ≥ 3.0.0 and ≤ 3.2.3, patched in 3.2.4) documents the reporter’s proof of concept on a default Docker deployment of 3.2.3: POST /api/terminals returned a terminal id, the WebSocket accepted whoami && id, and the shell answered uid=0(root). Those measurements are the reporter’s, not ours — but the mechanism needs no reproduction to be believed, because the diff is the whole story.

The channel the blocklist could never cover: CVE-2026-111058

CVE-2026-111058 (published 16:17:03.957Z, the same minute, Awaiting Analysis, CWE-862 missing authorization, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8) is the more important record of the pair, and its advisory says why in plain language. Mercury runs its Jupyter server with anonymous access by default: mercury_app/__main__.py sets --IdentityProvider.token='' when no token is configured, and a tokenless server additionally disables the cross-site request forgery check. The advisory for GHSA-rv4m-xm3f-3ccq (published 31 August 2026, affected ≤ 3.2.5, patched in 3.2.6) then states the structural point: /api/kernels is not in the blocklist, and it cannot be, because Mercury needs kernels to run notebooks. The idle-timeout wrapper (patch_kernel_websocket_handler() in mercury_app/idle_timeout.py) touched the timeout clock on each message and passed everything else — including execute_request — straight to the original handler. So any unauthenticated client could create a kernel and send it arbitrary Python.

The advisory proves the lineage explicitly: tested on Mercury 3.2.4, the release that fixed the comma, POST /api/terminals now returned 403 while POST /api/kernels returned 201 and executed attacker Python. The first fix worked exactly as designed and the server was still remotely exploitable, because the threat model — “anonymous users, live kernels, separated by a URL list” — was never coherent. The actual fix, commit 73a40ce (“Secure anonymous kernel execution”, 27 August 2026), is correspondingly large: a new mercury_app/execution/ subsystem (connection, handlers, policy, registry, manifest — hundreds of added lines), a new security_mode.py, and 34 deleted lines in __main__.py. A blocklist entry repairs an instance; this commit replaced the mechanism.

The sink outside the blocklist’s jurisdiction: CVE-2026-111059

CVE-2026-111059 (published 17:17:02.940Z, Awaiting Analysis, CWE-79, AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N = 6.1 medium) moves the problem from the server to the viewer. The mr.Markdown widget converts its input with python-markdown — which passes raw HTML through by default — and assigns the result to an ipywidgets.HTML value, which reaches the DOM as unsanitized innerHTML. There is no allowlist sanitizer anywhere in the package; the advisory for GHSA-2658-85cv-9wwq (published 11 September 2026, affected ≥ 3.0.0, patched in 3.2.11) notes that the one function with “sanitize” in its name, sanitize_notebook_for_mercury_runtime, strips runtime metadata and clears saved cell outputs — notebook hygiene, not XSS defense — and never touches widget values or Markdown source. The advisory’s threat model is careful and worth repeating: this fires when an app passes untrusted data — database rows, user submissions, uploaded files, or an external/LLM API response — to the widget, and it executes in the browser of whoever views the app. In a framework whose selling point is turning notebooks into shared dashboards fed by data, that is ordinary usage, not an edge case. The fix, commit c798a85 (“sanitize Markdown and HTML Message”, 10 September 2026), rewrote the rendering path (mercury/md.py +67/−29, plus mercury/chat/message.py) and updated the widget documentation the same day.

Three fixes, three silences, one lesson

Count the lags: the comma fix shipped 21 August and its CVE published 19 October (59 days); the kernel-execution rework shipped 28 August and its CVE published 19 October (52 days); the Markdown sanitization shipped 10 September and its CVE published 19 October (39 days). Every operator who upgraded Mercury for features during those windows was protected without knowing it, and every operator who pinned a version for stability ran a remotely exploitable notebook server — in the 10.0 case, handing out root shells — with no advisory, no changelog security section and no scanner signal to act on. We have now measured this exact pattern in Cognition, in Gitea’s 28.2.0 release and in BentoML’s Dockerfile injections. The responsible disclosure pipeline did everything right here — advisories with proofs, versioned fixes, credits — and the CVE-numbering lag still left a two-month window where the only document describing a critical defect was a GitHub advisory most scanners ingest late or never.

The deeper point belongs to anyone building on notebook-backed execution: a URL blocklist is not an authorization model for an anonymous kernel server. The comma bug is the memorable instance, but the kernel record is the durable finding — the maintainers’ own advisory concedes the blocklist could never have covered the endpoint that mattered most. If your agent or data product wraps Jupyter for anonymous or low-trust users, the question is not whether your denylist is complete. It is whether executable messages are authenticated per request. Mercury’s answer, after the second CVE, is a policy-and-registry execution layer. Anything thinner than that is the pre-August design.

What to do

Run Mercury 3.2.11 or later; it contains all three fixes, and there is no reason to stop at an intermediate version. If you run Mercury on any network — including inside a container on a shared host — set a token (MERCURY_TOKEN or --token) rather than accepting the tokenless default, and confirm anonymous clients cannot create kernels: one unauthenticated POST /api/kernels against your own deployment is the entire audit. If you ran anything below 3.2.4 reachable from untrusted networks, treat the host as compromised — the 10.0 gave interactive shells, and the reporter’s measurement shows them landing as root in the default container. If your apps render untrusted or model-generated content through mr.Markdown, 3.2.11 is the fix, and previously rendered views should be assumed to have delivered whatever the data contained. None of the three is in the CISA KEV catalogue (checked directly, 20 October 2026, 1,749 entries, zero hits), and no exploitation has been reported — which, given the 59-day window on the 10.0, is a statement about visibility rather than safety.

Our verification was read-only. The three CVE identifiers, publication timestamps of 2026-10-19T16:17:03.810Z, 16:17:03.957Z and 17:17:02.940Z, vulnStatus values, the security-advisories@github.com source identifier, descriptions, CWE assignments and CVSS 3.1 vectors and scores were read first-hand from the NVD 2.0 API. The affected ranges, patched versions, publication dates and quoted technical prose were read first-hand from the three public GitHub Security Advisories (GHSA-f8c7-rwf9-fmgm, GHSA-rv4m-xm3f-3ccq, GHSA-2658-85cv-9wwq). The one-line blocklist diff, the commit messages and timestamps, the per-file line counts of all three fix commits, the release tags and their publication timestamps (3.2.4 on 21 August, 3.2.6 on 28 August, v3.2.11 on 10 September 2026) and the repository metadata (4,357 stars, 292 forks, Apache-2.0, not archived) were read first-hand from the GitHub API. KEV status was checked directly against the published catalogue. Brave Search was used for discovery only and returned automated mirrors — OSV, the GitLab advisory database, strix.ai, Threat Radar and DailyCVE — with no independent technical analysis of any of the three records. We installed nothing, started no Mercury server, created no kernel or terminal, authored no Markdown payload, executed no proof of concept and sent no third-party request beyond reading the cited public records; every runtime result quoted above is the reporter’s measurement as documented in the advisories.

Sources: