Mercury: Stored / DOM Cross-Site Scripting (XSS): mr.Markdown renders untrusted content as raw, unsanitized HTML
## Summary 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): ```python 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"{escape(text)} " # 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: ``, ``, 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;
- Products
- mercury
- Affected versions
- 3.0.0, 3.0.1, 3.0.2, 3.1.0, 3.1.1, 3.1.2, 3.1.4, 3.1.5, 3.2.0, 3.2.1, 3.2.10, 3.2.2, 3.2.3, 3.2.4, 3.2.5, 3.2.6, 3.2.7, 3.2.8, 3.2.9
- Fixed versions
- 3.2.11
- CVEs
- CVE-2026-111059
- Confirmed fixed CVEs
- Not specified
- Updated in this feed
- 31 Oct 2026, 08:21 UTC · Revision 1
Source evidence
Mercury: Stored / DOM Cross-Site Scripting (XSS): mr.Markdown renders untrusted content as raw, unsanitized HTML
core-osv · Original sourcePublished 19 Oct 2026, 17:01 UTC
## Summary 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): ```python 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"{escape(text)} " # 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: ``, ``, 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;
osv:GHSA-2658-85cv-9wwq
mercury
Fixed versions: 3.2.11
Complete source record
{
"id": "3d1483f4d9390de76a292636b0b54a864862147a0bca0a416e74e644a5a31bab",
"url": "https://osv.dev/vulnerability/GHSA-2658-85cv-9wwq",
"method": "structured",
"payload": {
"kind": "advisory",
"title": "Mercury: Stored / DOM Cross-Site Scripting (XSS): mr.Markdown renders untrusted content as raw, unsanitized HTML",
"summary": "## Summary 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): ```python 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\"{escape(text)} \" # 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: ``, ``, 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;",
"evidence": "osv:GHSA-2658-85cv-9wwq",
"products": [
{
"name": {
"value": "mercury",
"evidence": "/affected/0/package/name"
},
"prerequisites": [],
"fixed_versions": [
{
"value": "3.2.11",
"evidence": "/affected/0/ranges"
}
],
"affected_versions": [
{
"value": "3.0.0",
"evidence": "/affected/0/versions"
},
{
"value": "3.0.1",
"evidence": "/affected/0/versions"
},
{
"value": "3.0.2",
"evidence": "/affected/0/versions"
},
{
"value": "3.1.0",
"evidence": "/affected/0/versions"
},
{
"value": "3.1.1",
"evidence": "/affected/0/versions"
},
{
"value": "3.1.2",
"evidence": "/affected/0/versions"
},
{
"value": "3.1.4",
"evidence": "/affected/0/versions"
},
{
"value": "3.1.5",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.0",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.1",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.10",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.2",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.3",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.4",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.5",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.6",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.7",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.8",
"evidence": "/affected/0/versions"
},
{
"value": "3.2.9",
"evidence": "/affected/0/versions"
}
]
}
],
"severity": "medium",
"structured": {
"id": "GHSA-2658-85cv-9wwq",
"aliases": [
"CVE-2026-111059",
"GHSA-2658-85cv-9wwq"
],
"metrics": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
}
],
"affected": [
{
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.2.11"
}
]
}
],
"package": {
"name": "mercury",
"purl": "pkg:pypi/mercury",
"ecosystem": "PyPI"
},
"versions": [
"3.0.0",
"3.0.1",
"3.0.2",
"3.1.0",
"3.1.1",
"3.1.2",
"3.1.4",
"3.1.5",
"3.2.0",
"3.2.1",
"3.2.10",
"3.2.2",
"3.2.3",
"3.2.4",
"3.2.5",
"3.2.6",
"3.2.7",
"3.2.8",
"3.2.9"
],
"database_specific": {
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-2658-85cv-9wwq/GHSA-2658-85cv-9wwq.json"
}
}
],
"modified": "2026-10-19T17:15:04.101718667Z",
"provider": "osv",
"references": [
"https://github.com/mljar/mercury/security/advisories/GHSA-2658-85cv-9wwq",
"https://github.com/mljar/mercury/commit/c798a85924e6614c6fea7afe5a560e4554a1d09b",
"https://github.com/mljar/mercury",
"https://github.com/mljar/mercury/releases/tag/v3.2.11"
],
"provider_metadata": {
"cwe_ids": [
"CWE-116",
"CWE-79"
],
"severity": "MODERATE",
"github_reviewed": true,
"nvd_published_at": null,
"github_reviewed_at": "2026-10-19T17:01:20Z"
}
},
"related_cves": [
{
"value": "CVE-2026-111059",
"evidence": "/aliases"
}
],
"security_relevant": true,
"severity_evidence": "/severity/0/score"
},
"source_id": "core-osv",
"document_id": 375520,
"published_at": "2026-10-19T17:01:20+00:00"
}