Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
MEDIUM severity

Mistune renderers/html.safe_url: HARMFUL_PROTOCOLS list misses legacy and chained schemes that historically chain to `javascript:` executionGHSA-qfrw-5rxm-mhh2

MEDIUMFix: lepture/mistune@c7101fc

GHSA-qfrw-5rxm-mhh2 is a medium-severity (CVSS 6.1) Cross-site Scripting (XSS) vulnerability in mistune. A fix is available for mistune — see the affected versions and patch details below.

Also known asCVE-2026-59929PYSEC-2026-2217
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-qfrw-5rxm-mhh2.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs26th percentile — riskier than 26% of all scored CVEsHighest risk
0.00%0.28%0.56%0.84%0.2%0.3%0.3%0.3%Aug 26Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-qfrw-5rxm-mhh2 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐍mistune

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

Type: URL-scheme allowlist gap. The safe_url filter only blocks the four schemes javascript:, vbscript:, file:, data:. Several other schemes are accepted into rendered <a href="..."> and <img src="..."> tags despite being known XSS vectors in legacy or chain-handling browsers. The same gap applies to direct links, reference links, and autolinks. File: src/mistune/renderers/html.py, line 11-23 (HARMFUL_PROTOCOLS list). Root cause: the HARMFUL_PROTOCOLS tuple is a hardcoded, opt-out denylist of four entries. Browsers historically supported (and some still partially support) several other schemes that either execute JavaScript directly (livescript:, mocha:) or wrap a javascript: payload (feed:javascript:, view-source:javascript:, jar:javascript:, ms-its:javascript:, mk:@MSITStore:javascript:). On user-agents that still recognise these schemes (older Firefox builds for feed:/jar:, all Internet Explorer / Edge Legacy for ms-its:/mk:/res:, niche chrome-style browsers, browser extensions that register custom protocol handlers), clicking a link rendered by mistune executes attacker-controlled JavaScript in the page's origin.

Affected Code

File: src/mistune/renderers/html.py, lines 10-62.

class HTMLRenderer(BaseRenderer):
    HARMFUL_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
        "javascript:",
        "vbscript:",
        "file:",
        "data:",
    )                                                         # <-- BUG: incomplete denylist
    GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
        "data:image/gif;",
        "data:image/png;",
        "data:image/jpeg;",
        "data:image/webp;",
    )

    def safe_url(self, url: str) -> str:
        if self._allow_harmful_protocols is True:
            return escape_text(url)
        _url = url.lower()
        if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):
            return escape_text(url)
        if _url.startswith(self.HARMFUL_PROTOCOLS) and not _url.startswith(self.GOOD_DATA_PROTOCOLS):
            return "#harmful-link"
        return escape_text(url)                               # <-- BUG: any scheme not in HARMFUL_PROTOCOLS passes through

Why it's wrong: an opt-out denylist for URL schemes is the wrong shape. The set of schemes a user-agent might honour is unbounded (registered handlers, browser extensions, OS-level protocol registrations, custom intent handlers on Android, etc.), but the set of schemes a markdown renderer needs to allow is small (http://, https://, mailto:, optionally tel:, ftp:, fragment-only #anchor, and a few image-only data: types). Switching to an opt-in allowlist with a safe_extra_protocols knob for callers who need others would close every variant of this bug class permanently. The current code accepts every chained-scheme XSS vector for as long as the project remembers to keep the denylist current.

Exploit Chain

  1. Application accepts attacker-supplied markdown and renders it with mistune. The default escape=True prevents raw HTML, but link href/image src filtering is the only XSS defense for [click](...) and ![alt](...) syntax.
  2. Attacker writes [click here](feed:javascript:alert(document.cookie)). mistune's safe_url checks feed:javascript:alert(document.cookie) against HARMFUL_PROTOCOLS = ('javascript:', 'vbscript:', 'file:', 'data:') — none match. The href is escape_text'd (HTML-entity escape) and emitted as <a href="feed:javascript:alert(document.cookie)">click here</a>.
  3. Victim using a Firefox build that still has the feed handler registered (extension, configuration, or LTS that retained the feed reader past the 64.0 removal — including some forks and ESR builds) clicks the link. Firefox's feed handler invokes the inner URL, which is javascript:alert(...). JS executes in the page's origin. Victim's session cookie is exfiltrated.
  4. Same pattern for livescript:alert(1) (Netscape Communicator era, still recognised by some niche browsers / browser-emulator tools), view-source:javascript:alert(1) (Firefox, see CVE-2009-1938), jar:javascript:alert(1) (older Firefox), ms-its:javascript: (IE/Edge Legacy), res:javascript: (IE), mk:@MSITStore:javascript: (IE CHM viewer). Each user-agent that recognises one of these is exploitable; the user-agent population that recognises at least one is not negligible (corporate environments still running Edge Legacy compatibility mode, locked-down kiosk browsers, Android WebView in apps that register custom intent handlers, Linux distros with old Firefox ESR plus the feed: extension, etc.).

The same primitive applies to image src (![](feed:javascript:...) rendered as <img src="feed:...">) — though most browsers don't fetch javascript: from img src, the same chained handler quirk applies on a few user-agents — and to reference links and autolinks (verified in the PoC below; the rendered HTML is identical regardless of which markdown link syntax is used).

Security Impact

Severity: sec-moderate. Conditional XSS depending on user-agent. Modern Chrome / Edge Chromium / Safari ignore most of these schemes, but Firefox forks, Edge Legacy, in-app WebViews, browser extensions registering custom handlers, and corporate browser deployments are exposed. Defence-in-depth is the framing: a markdown renderer should not need to track which browsers still honour which legacy chained-scheme. Attacker capability: plant a link in any place the application renders user-supplied markdown. When clicked by a user-agent that honours the legacy scheme, the attacker's JavaScript runs in the page's origin (steal cookies, perform actions as the victim, etc.). Preconditions: application uses mistune to render attacker-influenced markdown. Default config. Victim user-agent is one of the affected populations. No specific mistune option is required. Differential: PoC-verified against [email protected], default config. The following inputs all PASS the filter and reach the rendered HTML unchanged:

import mistune
md = mistune.create_markdown()
for url in [
    'feed:javascript:alert(1)',                  # Firefox feed handler chain
    'livescript:alert(1)',                       # Netscape, niche browsers
    'mocha:alert(1)',                            # Netscape, niche browsers
    'view-source:javascript:alert(1)',           # Firefox view-source chain (CVE-2009-1938 class)
    'jar:javascript:alert(1)',                   # Firefox jar: handler chain
    'ms-its:javascript:alert(1)',                # IE/Edge Legacy InfoTech Storage handler
    'mk:@MSITStore:javascript:alert(1)',         # IE CHM viewer chain
    'res:javascript:',                           # IE resource: handler
]:
    print(md(f'[click]({url})').strip())

# Output (each one passes the filter):
#   <p><a href="feed:javascript:alert(1)">click</a></p>
#   <p><a href="livescript:alert(1)">click</a></p>
#   <p><a href="mocha:alert(1)">click</a></p>
#   <p><a href="view-source:javascript:alert(1)">click</a></p>
#   <p><a href="jar:javascript:alert(1)">click</a></p>
#   <p><a href="ms-its:javascript:alert(1)">click</a></p>
#   <p><a href="mk:@MSITStore:javascript:">click</a></p>
#   <p><a href="res:javascript:">click</a></p>

For comparison, the four schemes already in the denylist are correctly blocked: javascript:, vbscript:, file:, data:text/html all return <a href="#harmful-link">.

The same gap applies to reference links ([click][ref]\n\n[ref]: feed:javascript:alert(1) → <a href="feed:javascript:alert(1)">) and to autolinks (<feed:javascript:alert(1)> → <a href="feed:javascript:alert(1)">).

Suggested Fix

Switch from denylist to allowlist. The set of schemes a markdown renderer needs to allow is small and well-known; the set of schemes that might trigger handler chains is unbounded.

--- a/src/mistune/renderers/html.py
+++ b/src/mistune/renderers/html.py
@@ -7,21 +7,28 @@ class HTMLRenderer(BaseRenderer):

     _escape: bool
     NAME: ClassVar[Literal["html"]] = "html"
-    HARMFUL_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
-        "javascript:",
-        "vbscript:",
-        "file:",
-        "data:",
-    )
+    SAFE_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
+        "http:",
+        "https:",
+        "mailto:",
+        "tel:",
+        "ftp:",
+        "ftps:",
+        "irc:",
+        "ircs:",
+    )
     GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
         "data:image/gif;",
         "data:image/png;",
         "data:image/jpeg;",
         "data:image/webp;",
     )

@@ -49,15 +56,21 @@ class HTMLRenderer(BaseRenderer):
     def safe_url(self, url: str) -> str:
-        if self._allow_harmful_protocols is True:
-            return escape_text(url)
-
-        _url = url.lower()
-        if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):
-            return escape_text(url)
-
-        if _url.startswith(self.HARMFUL_PROTOCOLS) and not _url.startswith(self.GOOD_DATA_PROTOCOLS):
-            return "#harmful-link"
-        return escape_text(url)
+        # Allow-list: only schemes in SAFE_PROTOCOLS, image-only data: URLs in
+        # GOOD_DATA_PROTOCOLS, scheme-relative URLs (//host/path), absolute
+        # paths (/path), and anchor-only references (#fragment) reach the
+        # rendered output. Everything else is replaced with '#harmful-link'.
+        if self._allow_harmful_protocols is True:
+            return escape_text(url)
+        _url = url.lower().lstrip()
+        if (
+            _url.startswith(self.SAFE_PROTOCOLS)
+            or _url.startswith(self.GOOD_DATA_PROTOCOLS)
+            or _url.startswith(("/", "#", "?"))
+            or ":" not in _url.split("/", 1)[0]   # bare relative path
+        ):
+            return escape_text(url)
+        if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):
+            return escape_text(url)
+        return "#harmful-link"

The allow_harmful_protocols option is preserved, so callers who genuinely want to allow a custom scheme can still opt in. The lower().lstrip() also closes the leading-whitespace evasion sub-case (e.g., javascript: is already blocked by the current code via lower().startswith, but the same pattern needs to apply on the new allowlist branch). Add regression tests for each scheme listed in the PoC above asserting they resolve to #harmful-link.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPImistuneall versions3.3.0pip install --upgrade 'mistune==3.3.0'

Affected Products

1 product · 1 configurations
Application
mistunemistune_project
< 3.3.0
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for mistune, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update mistune to 3.3.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-qfrw-5rxm-mhh2 is resolved across your whole dependency graph.

  3. Workarounds

    Escape or sanitise the affected output on the server side rather than relying on client-side filtering, and add a Content-Security-Policy that blocks inline script execution so injected markup cannot run even if it reaches the page.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatModerate

This Moderate-impact flaw in Mistune, a Python Markdown parser, allows for Cross-Site Scripting (XSS) due to insufficient filtering of URL schemes. Exploitation requires a user to process specially crafted Markdown content, which could lead to arbitrary script execution in the user browser. Red Hat products that…

Workaround published by Red Hat
No dedicated mitigation is available beyond upgrading to Mistune 3.3.0 or later, which fully blocks all dangerous URL schemes in the safe_url filter. As a temporary workaround, applications can wrap Mistune output in an additional allow-list-based URL sanitizer before rendering untrusted Markdown content, or avoid rendering Markdown from untrusted/attacker-controlled sources.
Source: Red Hat security advisory for GHSA-qfrw-5rxm-mhh2 (CC BY 4.0)

Frequently Asked Questions

## Summary **Type:** URL-scheme allowlist gap. The `safe_url` filter only blocks the four schemes `javascript:`, `vbscript:`, `file:`, `data:`. Several other schemes are accepted into rendered `<a href="...">` and `<img src="...">` tags despite being known XSS vectors in legacy or chain-handling browsers. The same gap applies to direct links, reference links, and autolinks. **File:** `src/mistune/renderers/html.py`, line 11-23 (HARMFUL_PROTOCOLS list). **Root cause:** the HARMFUL_PROTOCOLS tuple is a hardcoded, opt-out denylist of four entries. Browsers historically supported (and some still
O3 Security · Impact-Aware SCA

Is GHSA-qfrw-5rxm-mhh2 in your dependencies?

Find it across PyPI, including transitive dependencies.

Mistune renderers/html.safe_url: HARMFUL_PROTOCOLS…