CVE-2026-111059

CVE-2026-111059 is a medium-severity cross-site scripting (XSS) vulnerability in mercury (pip), affecting versions >= 3.0.0, < 3.2.11. It is fixed in 3.2.11.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

Mercury: Stored / DOM Cross-Site Scripting (XSS): mr.Markdown renders untrusted content as raw, unsanitized HTML

Mercury turns Jupyter notebooks into shared web apps/dashboards. The mr.Markdown
widget converts its input to HTML with python-markdown and no sanitization
step
. MarkdownWidget is an ipywidgets.HTML widget, whose value is written to
the page as raw HTML (via innerHTML) with no sanitization, so the result
reaches the DOM verbatim. When an app passes untrusted data (database rows,
user submissions, uploaded file contents, or an external/LLM API response) to
mr.Markdown, HTML/JS in that data executes in the browser of whoever views the
app
, a data-driven stored/DOM-based XSS.

This is a library-provided rendering sink that fails to neutralize output; it is
triggered by ordinary, reasonable application code, not by an author mistake.

Root cause

mercury/md.py, MarkdownWidget._to_html() (verified at v3.2.6, lines ~57–65):

def _to_html(self, text: str) -> str:
    if md_lib is not None:
        body = md_lib.markdown(text)          # python-markdown: passes raw HTML through
    else:
        body = f"<pre>{escape(text)}</pre>"   # escape() is ONLY the md-lib-absent fallback
    return self._apply_inline_theme(body)     # regex styling, adds style=, removes nothing

Two neutralization failures compound:

  1. Markdown → HTML is unsanitized. python-markdown passes raw HTML through by
    default: <img src=x onerror=…>, <svg onload=…>, and javascript: links
    survive verbatim. There is no bleach/nh3 allowlist anywhere in the package.
    _apply_inline_theme() only injects inline style= attributes via regex; it is
    not a sanitizer and removes nothing. escape() runs only in the
    md-lib-absent fallback.

  2. The rendering sink is unsanitized innerHTML. MarkdownWidget
    (mercury/md.py:21) subclasses ipywidgets.HTML. The ipywidgets HTML widget
    renders its value into the DOM as raw HTML via innerHTML, with no
    sanitization (this is the widget's design). Event-handler payloads such as
    <img src=x onerror=…> therefore execute when the widget is displayed.

On the "sanitize" that exists: mercury_app/notebook_sanitize.py
(sanitize_notebook_for_mercury_runtime) and packages/lab/src/mercury/save.ts
(sanitizeMercuryNotebookContent) strip runtime metadata and clear code-cell
saved outputs
. They do not neutralize HTML/JS, and they do not touch widget
values or markdown source. "Sanitize" here is notebook hygiene, not XSS defense.

Threat model (attacker → victim, not self-inflicted)

The CVE-relevant framing is data-driven: the app author writes ordinary code,
and the data is attacker-controlled.

import mercury as mr
# `row` comes from a DB / user submission / uploaded file / external API , 
# i.e. NOT authored by the app developer:
mr.Markdown(row["comment"])

The attacker supplies the untrusted value (posts the comment, uploads the file,
poisons the API/RAG source). A different user, another app user, a reviewer,
an admin, then opens the app, and the payload executes in their browser. The
vulnerable component (Mercury's server-side widget rendering) and the impacted
component (the victim's browser session in the app origin) differ, which is the
classic stored-XSS attacker/victim separation.

Amplifier, session sharing (--keep-session). Mercury can serve one shared
kernel session to all viewers (mercury run … --keep-session, README "Arguments":
"share the same session between multiple users … others will see this"). It is
opt-in and defaults to off (keepSession=False in mercury_app/handlers.py),
so it is not required for the base case, any app that renders one user's
untrusted data to another user is already vulnerable. When enabled, content one
viewer submits is rendered into the session other viewers see, extending this to
cross-user delivery. Authentication is opt-in and off by default: a free
password gate exists (mercury run … --pass=…, README), while user-based
authentication is a paid add-on. Because auth is off unless the operator enables
it, unauthenticated public deployments, where the attacker needs no account, are
a realistic default.

Proof of Concept

import mercury as mr

# Value that would realistically arrive from untrusted data (DB row, form field,
# uploaded file, API/LLM response):
untrusted = '<img src=x onerror=alert(String.fromCharCode(88,83,83))>'

mr.Markdown(untrusted)   # rendered to raw HTML, unsanitized, into the app DOM

The generated widget HTML contains the payload verbatim; when the widget is
displayed, the onerror handler runs.

What was confirmed (and how)

  • Confirmed by execution in a real browser (sink level): driving the actual
    python-markdown output
    into the same ipywidgets.HTML value sink the widget
    uses causes the payload to execute in headless Chromium. Execution was captured
    via Playwright's dialog event (which fires only on a genuine alert(), so the
    mere presence of the string in the HTML cannot cause a false positive). A benign
    control (**hello**) produced zero dialogs, confirming the harness reports
    only real execution. python-markdown passes <img src=x onerror=…> through
    verbatim by default (behavior is not version-specific).
  • Reasoned from source (not executed end-to-end): the full path from a
    mr.Markdown(...) call in a served Mercury app to a viewer's DOM was not run
    end-to-end, because no isolated container/VM was available in the reporting
    environment. Because MarkdownWidget is the ipywidgets.HTML sink above and
    Mercury's frontend applies no sanitizer to widget HTML values (the only frontend
    sanitizer strips notebook metadata at save time), the served-app path is expected
    to behave identically. Reproduce end-to-end before publishing if you want the
    advisory to state full-path browser confirmation.

Severity rationale (CVSS 3.1 per-metric)

Base score reflects the realistic default: an unauthenticated deployment whose app
renders untrusted data via mr.Markdown.

Metric Value Reason
AV Attack Vector Network (N) App is served over HTTP to remote users.
AC Attack Complexity Low (L) No special conditions or race; supply data that reaches an mr.Markdown call.
PR Privileges Required None (N) Baseline: the injection channel (public form/upload/API) needs no account; auth is optional/paid. See variant below.
UI User Interaction Required (R) A victim must open/view the app page for the stored payload to run.
S Scope Changed (C) The vulnerable component (server-side widget render) injects script that executes in a different security authority, the victim's browser session/origin. (This is the canonical XSS scoping in the CVSS 3.1 spec.)
C Confidentiality Low (L) Script can read data/session material available to the app origin; scored L (not H) for a generic app without asserting full account takeover.
I Integrity Low (L) Script can alter the page and act as the victim within the app; scored L generically.
A Availability None (N) No direct availability impact.

Result: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N = 6.1 (Medium).

Deployment-dependent variants (state your environment's when filing):

  • Injection channel requires an authenticated low-privilege user → PR:L →
    …/PR:L/UI:R/S:C/C:L/I:L/A:N = 5.4.
  • App origin exposes high-value auth material such that XSS yields full
    session/account takeover → C:H/I:H raises the score; justify per deployment
    rather than claiming it universally.

Additional hardening (secondary, not the reported vulnerability)

Several widget ESMs write the label with innerHTML, e.g. mercury/text.py
topLabel.innerHTML = model.get("label") || … (same shape in number.py,
time.py, date/datetime/daterange/select/json/file, and
indicator.py's _card_html). This is not an attacker→victim XSS path: labels
are static strings set by the app author in Python; URL/url_params overrides
affect only widget value (assigned via the safe input.value property), never
label, so there is no end-user/URL route into a label. It is a defense-in-depth /
author footgun only. Recommended regardless: render UI strings with textContent,
or html.escape labels, so output encoding is applied uniformly.

Impact

Mercury shares notebooks as web apps/dashboards, often for non-technical audiences.
When an app renders untrusted data through mr.Markdown, the generated page carries
attacker-controlled script that runs in the browser of any other viewer, in the
app origin, enabling session/cookie/token theft, UI spoofing and forced actions as
the victim, and pivoting within the hosting origin. With --keep-session, this
extends to cross-user delivery within a shared session.

Untrusted input is rendered as active markup in a victim's browser, which can run script in their session. Typical impact: session or credential theft, and actions taken as the user.

CVE-2026-111059 has a CVSS score of 6.1 (Medium). The vector is network-reachable, no privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (3.2.11); upgrading removes the vulnerable code path.

Affected versions

mercury (>= 3.0.0, < 3.2.11)

Security releases

mercury → 3.2.11 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Primary, sanitize by default in mr.Markdown, with an explicit opt-out for the
deliberate raw-HTML use case
(backward-compatible, non-breaking):

# mercury/md.py
def _to_html(self, text: str, unsafe_allow_html: bool = False) -> str:
    if md_lib is not None:
        body = md_lib.markdown(text)
    else:
        body = f"<pre>{escape(text)}</pre>"
    if not unsafe_allow_html:
        # allowlist tags/attributes; strip javascript:/data: schemes and event handlers
        body = nh3.clean(body)            # or bleach.clean(..., strip=True)
    return self._apply_inline_theme(body)

Expose unsafe_allow_html=False on mr.Markdown(...) so authors who deliberately
embed trusted raw HTML can opt in, while the default is safe. This mirrors the
convention used by comparable tools (e.g. Streamlit's unsafe_allow_html).

Defense-in-depth: since widget HTML values are written via innerHTML without
sanitization, consider sanitizing widget HTML values at the frontend before they
reach the DOM, and/or serving a Content-Security-Policy that forbids inline event
handlers, so a single missed sink is not immediately exploitable.

Frequently Asked Questions

  1. What is CVE-2026-111059? CVE-2026-111059 is a medium-severity cross-site scripting (XSS) vulnerability in mercury (pip), affecting versions >= 3.0.0, < 3.2.11. It is fixed in 3.2.11. Untrusted input is rendered as active markup in a victim's browser, which can run script in their session.
  2. How severe is CVE-2026-111059? CVE-2026-111059 has a CVSS score of 6.1 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of mercury are affected by CVE-2026-111059? mercury (pip) versions >= 3.0.0, < 3.2.11 is affected.
  4. Is there a fix for CVE-2026-111059? Yes. CVE-2026-111059 is fixed in 3.2.11. Upgrade to this version or later.
  5. Is CVE-2026-111059 exploitable, and should I be worried? Whether CVE-2026-111059 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-111059 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-111059? Upgrade mercury to 3.2.11 or later.

Other vulnerabilities in mercury

Stop the waste.
Protect your environment with Kodem.