VAITP Dataset

← Back to the dataset

CVE-2026-59930

Mistune ToC generates predictable IDs, allowing anchor collision and hijacking.

  • CVSS 4.3
  • CWE-345
  • Input Validation and Sanitization
  • Remote

Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.3.0, the toc plugin and TableOfContents directive generate heading IDs as predictable toc_N values without slugifying the heading text, allowing attacker-controlled id="toc_N" content to collide with generated anchors and redirect same-page navigation, CSS selectors, or JavaScript handlers. This issue is fixed in version 3.3.0.

CVSS base score
4.3
Published
2026-07-08
OWASP
A03 Injection
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Algorithm
Category
Input Validation and Sanitization
Subcategory
Cross-Site Scripting (XSS)
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
Mistune
Fixed by upgrading
Yes

Solution

Upgrade Mistune to version 3.3.0 or later.

Vulnerable code sample

import mistune
from mistune.plugins import plugin_toc

# This code represents the behavior of mistune < 3.3.0.
# An attacker provides Markdown with an embedded HTML element that has a
# predictable ID, "toc_2".
malicious_markdown = """
# First Heading

This is some legitimate content.

<div id="toc_2">
  <p>This is an attacker-controlled element. The link for "Second Legitimate Heading" will incorrectly point here.</p>
</div>

# Second Legitimate Heading

This is the content that should be the target of the second TOC link, but its ID will collide with the element above.

# Third Legitimate Heading

More content.
"""

# The state dictionary is used by the plugin to store the generated TOC data.
state = {}

# Initialize the markdown parser with `escape=False` to allow raw HTML injection.
# The `plugin_toc` in vulnerable versions creates sequential IDs like toc_1, toc_2, etc.
markdown_parser = mistune.create_markdown(
    escape=False,
    plugins=[plugin_toc(state=state)]
)

# Render the markdown.
# The resulting HTML will contain two elements with `id="toc_2"`.
# In a browser, a link to `#toc_2` will navigate to the first one encountered,
# which is the attacker's injected div.
html_output = markdown_parser(malicious_markdown)

# The following would print the problematic HTML and the generated TOC.
# print("--- Rendered HTML ---")
# print(html_output)
# print("\n--- Generated TOC Data ---")
# print(state['toc'])
#
# Expected 'toc' data:
# [(1, 'First Heading', 'toc_1'), (1, 'Second Legitimate Heading', 'toc_2'), (1, 'Third Legitimate Heading', 'toc_3')]
# A TOC renderer would create a link <a href="#toc_2">, which leads to the attacker's div.

Patched code sample

import mistune
from mistune.plugins import plugin_toc

# In vulnerable versions of mistune (<2.0.3), the Table of Contents (TOC)
# plugin would generate predictable header IDs like `id="toc_1"`, `id="toc_2"`, etc.
# An attacker could inject HTML with a conflicting id (e.g., `<p id="toc_1">`)
# to disrupt navigation or CSS.

# The fix, implemented in mistune >=2.0.3 (and subsequent major versions like 3.x),
# is to generate IDs by "slugifying" the header text itself. This example
# demonstrates the fixed, secure behavior.

# Markdown content with headers
markdown_text = (
    "# First Header\n"
    "Some content here.\n\n"
    "## Sub-header with spaces & symbols!\n"
    "More content."
)

# Initialize mistune with the Table of Contents plugin.
# In a fixed version, this will automatically use the slugify function
# to generate unique, content-based IDs for headers.
markdown_parser = mistune.create_markdown(plugins=[plugin_toc()])

# Render the markdown to HTML
html_output = markdown_parser(markdown_text)

# Print the result.
# The 'id' attributes are now based on the header text ("slugified"),
# e.g., "first-header", and are no longer predictable "toc_N" values.
# This prevents ID collisions and demonstrates the fix for the vulnerability.
print(html_output)

# Expected output from a fixed version:
# <h1 id="first-header">First Header</h1>
# <p>Some content here.</p>
# <h2 id="sub-header-with-spaces-symbols">Sub-header with spaces & symbols!</h2>
# <p>More content.</p>

Payload

.. toc::

<div id="toc_1">
  <h2>Attacker Content</h2>
  <p>The Table of Contents link for "Legitimate Section" incorrectly jumps here.</p>
</div>

# Legitimate Section
This is the real content for the first section.

Cite this entry

@misc{vaitp:cve202659930,
  title        = {{Mistune ToC generates predictable IDs, allowing anchor collision and hijacking.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-59930},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59930/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::