CVE-2026-59929: Mistune renderers/html.safe_url: HARMFUL_PROTOCOLS list misses legacy and chained schemes that historically chain to `javascript:` execution

Published Jul 8, 2026
·
Updated

Summary

Type: URL-scheme allowlist gap. The safeurl 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 (HARMFULPROTOCOLS list). Root cause: the HARMFULPROTOCOLS 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.

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

def safeurl(self, url: str) -> str: if self.allowharmfulprotocols is True: return escapetext(url) url = url.lower() if self.allowharmfulprotocols and url.startswith(tuple(self.allowharmfulprotocols)): return escapetext(url) if url.startswith(self.HARMFULPROTOCOLS) and not url.startswith(self.GOODDATAPROTOCOLS): return "#harmful-link" return escapetext(url) # <-- BUG: any scheme not in HARMFULPROTOCOLS 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 safeextraprotocols 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). mistune's safeurl checks feed:javascript:alert(document.cookie) against HARMFULPROTOCOLS = ('javascript:', 'vbscript:', 'file:', 'data:') — none match. The href is escapetext'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 (! 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 mistune@3.2.1, default config. The following inputs all PASS the filter and reach the rendered HTML unchanged:

python import mistune md = mistune.createmarkdown() 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').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.

diff --- 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" - HARMFULPROTOCOLS: ClassVar[Tuple[str, ...]] = ( - "javascript:", - "vbscript:", - "file:", - "data:", - ) + SAFEPROTOCOLS: ClassVar[Tuple[str, ...]] = ( + "http:", + "https:", + "mailto:", + "tel:", + "ftp:", + "ftps:", + "irc:", + "ircs:", + ) GOODDATAPROTOCOLS: ClassVar[Tuple[str, ...]] = ( "data:image/gif;", "data:image/png;", "data:image/jpeg;", "data:image/webp;", )

@@ -49,15 +56,21 @@ class HTMLRenderer(BaseRenderer): def safeurl(self, url: str) -> str: - if self.allowharmfulprotocols is True: - return escapetext(url) - - url = url.lower() - if self.allowharmfulprotocols and url.startswith(tuple(self.allowharmfulprotocols)): - return escapetext(url) - - if url.startswith(self.HARMFULPROTOCOLS) and not url.startswith(self.GOODDATAPROTOCOLS): - return "#harmful-link" - return escapetext(url) + # Allow-list: only schemes in SAFEPROTOCOLS, image-only data: URLs in + # GOODDATAPROTOCOLS, 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.allowharmfulprotocols is True: + return escapetext(url) + url = url.lower().lstrip() + if ( + url.startswith(self.SAFEPROTOCOLS) + or url.startswith(self.GOODDATAPROTOCOLS) + or url.startswith(("/", "#", "?")) + or ":" not in url.split("/", 1)[0] # bare relative path + ): + return escapetext(url) + if self.allowharmfulprotocols and url.startswith(tuple(self.allowharmfulprotocols)): + return escapetext(url) + return "#harmful-link"

The allowharmfulprotocols 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.

Other sources

Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.3.0, the safeurl filter in src/mistune/renderers/html.py blocks only javascript:, vbscript:, file:, and data: schemes, allowing legacy or chained schemes such as feed:, view-source:, jar:, livescript:, mocha:, ms-its:, mk:, and res: to reach rendered href and src attributes and potentially execute script in affected user agents. This issue is fixed in version 3.3.0.

MITRE

Affected Software

3 affected componentsFixes available
Mistune Mistune<3.3.0
Mistune Project Mistune<3.3.0
pip/mistune<3.3.0
3.3.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/mistune to a version that resolves this vulnerability.

    Fixed in 3.3.0
  2. Upgrade

    Upgrade mistune to a version that resolves this vulnerability.

    Fixed in 3.3.0
  3. Configuration

    Replace the hardcoded HARMFUL_PROTOCOLS denylist with an allowlist implementation in HTMLRenderer.safe_url; regression-test each PoC scheme (feed:, jar:, livescript:, mk:@MSITStore:javascript:, mocha:, ms-its:javascript:, res:javascript:, view-source:javascript:) to assert they resolve to '#harmful-link'.

    mistune HTMLRenderer (src/mistune/renderers/html.py) safe_url SAFE_PROTOCOLS / allowlist-based scheme filtering = Use an allowlist (SAFE_PROTOCOLS + GOOD_DATA_PROTOCOLS) so only http:, https:, ftp:, ftps:, irc:, ircs:, mailto:, tel:, scheme-relative //..., absolute paths (/...), and anchor-only #... are allowed (and image-only data: types via GOOD_DATA_PROTOCOLS). Ensure the new logic applies to lowercase/leading-whitespace via lower().lstrip().

Event History

Jul 8, 2026
CVE Published
via MITRE·04:20 PM
Data Sourced
via MITRE·04:20 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·05:17 PM
RemedyDescriptionSeverityWeaknessAffected Software
Jul 20, 2026
Advisory Published
via GitHub·09:35 PM
Data Sourced
via GitHub·09:35 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-59929?

The severity of CVE-2026-59929 is rated medium with a CVSS score of 6.1.

2

How do I fix CVE-2026-59929?

To remediate CVE-2026-59929, update Mistune to version 3.3.0 or later.

3

What vulnerabilities are associated with CVE-2026-59929?

CVE-2026-59929 is associated with cross-site scripting (XSS) vulnerabilities due to the lack of filtering for certain harmful protocols.

4

What components are affected by CVE-2026-59929?

CVE-2026-59929 affects the safe_url filter in Mistune's HTML renderer prior to version 3.3.0.

5

Can legacy and chained schemes lead to exploits in CVE-2026-59929?

Yes, the vulnerability allows for exploitation through legacy and chained protocols that execute harmful actions.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203