GHSA-v87v-83h2-53w7 Mistune Heading ID Attribute has Injection XSS
Summary`HTMLRenderer.heading()` builds the opening `<hN>` tag by string-concatenating the `id` attribute value directly into the HTML — with no call to `escape()`, `safe_entity()`, or any other sanitisation function. A double-quote character `"` in the `id` value terminates the attribute, allowing an attacker to inject arbitrary additional attributes (event handlers, `src=`, `href=`, etc.) into the heading element.The default TOC hook assigns safe auto-incremented IDs (`toc_1`, `toc_2`, …) that never contain user text. However, the `add_toc_hook()` API accepts a caller-supplied `heading_id` callback. Deriving heading IDs from the heading text itself — to produce human-readable slug anchors like `#installation` or `#getting-started` — is by far the most common real-world usage of this callback (every major documentation generator does this). When the callback returns raw heading text, an attacker who controls heading content can break out of the `id=` attribute.Details**File:** `src/mistune/renderers/html.py`The `text` body (line content) *is* escaped upstream by the inline token renderer, which is why `text` arrives as `&quot;` etc. But `_id` arrives as a raw string directly from whatever the `heading_id` callback returned — no escaping occurs at any point in the pipeline.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 `heading_id` callback). The default hook generates sequential numeric IDs:Output — ID is auto-generated, no user text appears in it:**Step 2 — Add the realistic trigger: a text-based `heading_id` callback**Deriving an anchor ID from the heading text is the standard real-world pattern (slugifiers, `mkdocs`, `sphinx`, `jekyll` all do this). The PoC uses the simplest possible version — return the raw heading text unchanged — to show the vulnerability without any extra transformation:**Step 3 — Craft the exploit payload**Construct a heading whose text contains a double-quote followed by an injected attribute:When `raw_id` is called, `token["text"]` is `foo" onmouseover="alert(document.cookie)" x="`. This is passed verbatim to `heading()` as the `id` attribute value.**Step 4 — Observe attribute breakout in the output**Actual output:Note: the heading **body text** is correctly escaped (`&quot;`), but the **`id=` attribute** is not. A user who moves their mouse over the heading triggers `alert(document.cookie)`. Any JavaScript payload can be substituted.ScriptA verification script was created to verify this issue. It creates a HTML page showing the bypass rendering in the browser.Example Usage:Once the script is run, open `report_h2.html` in the browser and observe the behaviour.Impact| Dimension        | Assessment |
|------------------|-----------|
| **Confidentiality** | Session cookie / auth token theft via JavaScript execution triggered on mouse interaction |
| **Integrity**    | DOM manipulation, phishing content injection, forced navigation |
| **Availability** | Page freeze or crash available to attacker |**Risk context:** This vulnerability targets the most common customisation point for heading IDs. Any documentation site, wiki, or blog engine that generates slug-style anchors from heading text is vulnerable if it uses mistune's `heading_id` callback without independently sanitising the returned value.
CVSS v3.1 score: 6.1