CVE-2026-59929: Mistune HARMFUL_PROTOCOLS List Misses Legacy And Vulnerability

MODERATE
Advisory Identifier: GHSA-qfrw-5rxm-mhh2
CVSS 6.1
CWE-79 CWE-184
Published
Advisory Summary

CVE-2026-59929 is a MODERATE (CVSS 6.1) harmful_protocols list misses legacy and vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting Mistune libraries in the PyPI ecosystem. The issue involves harmful_protocols list misses legacy and chained schemes that historically chain to `javascript:` execution. Remediation is available by upgrading affected packages to version 3.3.0.

CVE-2026-59929 at a Glance

CVE Identifier CVE-2026-59929
GHSA Identifier GHSA-qfrw-5rxm-mhh2
Severity MODERATE
CVSS v3.1 Base Score 6.1
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product Mistune
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 6.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-59929

To remediate this vulnerability, upgrade all affected Mistune dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

mistune fixed: 3.3.0
pip install mistune>=3.3.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show mistune

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-59929, inspect your installed packages using pip:

pip show mistune

Analyze the terminal output to locate any version of Mistune in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.3.0), your application is affected.

Note: The pip show mistune command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In Mistune, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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. ```python 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 mistune@3.2.1, default config. The following inputs all PASS the filter and reach the rendered HTML unchanged: ```python 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. ```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" - 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`.

CVE-2026-59929 Affected Mistune Versions

1 component(s)
Fixed Versions:
3.3.0
Product / Package Ecosystem Affected Versions Fixed Version
mistune
PyPI
< 3.3.0
3.3.0

References

Frequently Asked Questions

What is CVE-2026-59929?
CVE-2026-59929 is a security vulnerability in Mistune involving HARMFUL_PROTOCOLS list misses legacy and chained schemes that historically chain to `javascript:` execution under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which Mistune versions are affected?
Affected versions include releases prior to 3.3.0 across affected packages: mistune.
What version fixes CVE-2026-59929?
Upgrading to 3.3.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is mistune affected?
Yes, mistune is affected in releases prior to 3.3.0. Upgrade your dependency to 3.3.0 or later.
What is the CVSS score of CVE-2026-59929?
CVE-2026-59929 has a CVSS v3.1 base score of 6.1 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N.
What CWE is associated with CVE-2026-59929?
CVE-2026-59929 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check PyPI dependencies for CVE-2026-59929?
Run the dependency tree command: 'pip show mistune'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-qfrw-5rxm-mhh2",
  "modified" : "2026-07-20T21:35:30Z",
  "published" : "2026-07-20T21:35:30Z",
  "aliases" : [ "CVE-2026-59929" ],
  "summary" : "Mistune renderers/html.safe_url: HARMFUL_PROTOCOLS list misses legacy and chained schemes that historically chain to `javascript:` execution",
  "details" : "## Summary\n\n**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.\n**File:** `src/mistune/renderers/html.py`, line 11-23 (HARMFUL_PROTOCOLS list).\n**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.\n\n## Affected Code\n\n**File:** `src/mistune/renderers/html.py`, lines 10-62.\n\n```python\nclass HTMLRenderer(BaseRenderer):\n    HARMFUL_PROTOCOLS: ClassVar[Tuple[str, ...]] = (\n        \"javascript:\",\n        \"vbscript:\",\n        \"file:\",\n        \"data:\",\n    )                                                         # <-- BUG: incomplete denylist\n    GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = (\n        \"data:image/gif;\",\n        \"data:image/png;\",\n        \"data:image/jpeg;\",\n        \"data:image/webp;\",\n    )\n\n    def safe_url(self, url: str) -> str:\n        if self._allow_harmful_protocols is True:\n            return escape_text(url)\n        _url = url.lower()\n        if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):\n            return escape_text(url)\n        if _url.startswith(self.HARMFUL_PROTOCOLS) and not _url.startswith(self.GOOD_DATA_PROTOCOLS):\n            return \"#harmful-link\"\n        return escape_text(url)                               # <-- BUG: any scheme not in HARMFUL_PROTOCOLS passes through\n```\n\n**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.\n\n## Exploit Chain\n\n1. 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.\n2. 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>`.\n3. 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.\n4. 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.).\n\nThe 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).\n\n## Security Impact\n\n**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.\n**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.).\n**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.\n**Differential:** PoC-verified against mistune@3.2.1, default config. The following inputs all PASS the filter and reach the rendered HTML unchanged:\n\n```python\nimport mistune\nmd = mistune.create_markdown()\nfor url in [\n    'feed:javascript:alert(1)',                  # Firefox feed handler chain\n    'livescript:alert(1)',                       # Netscape, niche browsers\n    'mocha:alert(1)',                            # Netscape, niche browsers\n    'view-source:javascript:alert(1)',           # Firefox view-source chain (CVE-2009-1938 class)\n    'jar:javascript:alert(1)',                   # Firefox jar: handler chain\n    'ms-its:javascript:alert(1)',                # IE/Edge Legacy InfoTech Storage handler\n    'mk:@MSITStore:javascript:alert(1)',         # IE CHM viewer chain\n    'res:javascript:',                           # IE resource: handler\n]:\n    print(md(f'[click]({url})').strip())\n\n# Output (each one passes the filter):\n#   <p><a href=\"feed:javascript:alert(1)\">click</a></p>\n#   <p><a href=\"livescript:alert(1)\">click</a></p>\n#   <p><a href=\"mocha:alert(1)\">click</a></p>\n#   <p><a href=\"view-source:javascript:alert(1)\">click</a></p>\n#   <p><a href=\"jar:javascript:alert(1)\">click</a></p>\n#   <p><a href=\"ms-its:javascript:alert(1)\">click</a></p>\n#   <p><a href=\"mk:@MSITStore:javascript:\">click</a></p>\n#   <p><a href=\"res:javascript:\">click</a></p>\n```\n\nFor comparison, the four schemes already in the denylist are correctly blocked: `javascript:`, `vbscript:`, `file:`, `data:text/html` all return `<a href=\"#harmful-link\">`.\n\nThe 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)\">`).\n\n## Suggested Fix\n\nSwitch 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.\n\n```diff\n--- a/src/mistune/renderers/html.py\n+++ b/src/mistune/renderers/html.py\n@@ -7,21 +7,28 @@ class HTMLRenderer(BaseRenderer):\n\n     _escape: bool\n     NAME: ClassVar[Literal[\"html\"]] = \"html\"\n-    HARMFUL_PROTOCOLS: ClassVar[Tuple[str, ...]] = (\n-        \"javascript:\",\n-        \"vbscript:\",\n-        \"file:\",\n-        \"data:\",\n-    )\n+    SAFE_PROTOCOLS: ClassVar[Tuple[str, ...]] = (\n+        \"http:\",\n+        \"https:\",\n+        \"mailto:\",\n+        \"tel:\",\n+        \"ftp:\",\n+        \"ftps:\",\n+        \"irc:\",\n+        \"ircs:\",\n+    )\n     GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = (\n         \"data:image/gif;\",\n         \"data:image/png;\",\n         \"data:image/jpeg;\",\n         \"data:image/webp;\",\n     )\n\n@@ -49,15 +56,21 @@ class HTMLRenderer(BaseRenderer):\n     def safe_url(self, url: str) -> str:\n-        if self._allow_harmful_protocols is True:\n-            return escape_text(url)\n-\n-        _url = url.lower()\n-        if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):\n-            return escape_text(url)\n-\n-        if _url.startswith(self.HARMFUL_PROTOCOLS) and not _url.startswith(self.GOOD_DATA_PROTOCOLS):\n-            return \"#harmful-link\"\n-        return escape_text(url)\n+        # Allow-list: only schemes in SAFE_PROTOCOLS, image-only data: URLs in\n+        # GOOD_DATA_PROTOCOLS, scheme-relative URLs (//host/path), absolute\n+        # paths (/path), and anchor-only references (#fragment) reach the\n+        # rendered output. Everything else is replaced with '#harmful-link'.\n+        if self._allow_harmful_protocols is True:\n+            return escape_text(url)\n+        _url = url.lower().lstrip()\n+        if (\n+            _url.startswith(self.SAFE_PROTOCOLS)\n+            or _url.startswith(self.GOOD_DATA_PROTOCOLS)\n+            or _url.startswith((\"/\", \"#\", \"?\"))\n+            or \":\" not in _url.split(\"/\", 1)[0]   # bare relative path\n+        ):\n+            return escape_text(url)\n+        if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):\n+            return escape_text(url)\n+        return \"#harmful-link\"\n```\n\nThe `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`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "mistune"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.3.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/lepture/mistune/security/advisories/GHSA-qfrw-5rxm-mhh2"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-59929"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/lepture/mistune/commit/c7101fcbb6e8790e8e39157c5ca2238fc6dd6cbc"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/lepture/mistune"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/lepture/mistune/releases/tag/v3.3.0"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/advisory-database/tree/main/vulns/mistune/PYSEC-2026-2217.yaml"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-184", "CWE-79" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-20T21:35:30Z",
    "nvd_published_at" : "2026-07-08T17:17:28Z"
  }
}