CVE-2026-59930: Mistune toc / TableOfContents directive: heading IDs use predictable `toc_N` numbering with no slugification, allowing collision with attacker-controlled `id="toc_N"` content

Published Jul 8, 2026
·
Updated

Summary

Type: Predictable identifier generation. The toc plugin and TableOfContents directive both default to generating heading IDs of the form toc1, toc2, toc3, ... with no input-derived component. An attacker who can place a heading anywhere in the document can predict which tocN ID it will receive, and can inject HTML elsewhere (in a non-heading context) that uses the same id="tocN" to either (a) shadow the legitimate heading anchor, breaking same-page navigation, or (b) collide with CSS or JavaScript that targets #tocN selectors, redirecting click handlers and styling to attacker-chosen content. File: src/mistune/toc.py line 36-37 (headingid = lambda token, index: "toc" + str(index + 1)); src/mistune/directives/toc.py line 33 (same default). Root cause: the default headingid callback ignores the heading's text content and uses only the headings's order in the document. Two documents rendered together (or one document with attacker-influenced headings spliced into trusted content) produce overlapping tocN IDs. Because id attribute uniqueness is required by HTML, browsers behaviour on duplicate IDs is undefined; document.getElementById('toc1') returns the first match, getElementsByTagName + querySelector semantics differ across paths, and CSS rules targeting #toc1 apply to whichever element matches first in tree order.

Affected Code

File: src/mistune/toc.py, lines 33-39.

python def addtochook(md, minlevel=1, maxlevel=3, headingid=None): if headingid is None: def headingid(token, index): return "toc" + str(index + 1) # <-- BUG: index-only ID, no slug derived from heading text

File: src/mistune/directives/toc.py, lines 32-33.

python class TableOfContents(DirectivePlugin): def init(self, minlevel=1, maxlevel=3): # ...

def generateheadingid(self, token, index): return "toc" + str(index + 1) # <-- BUG: same predictable scheme

Why it's wrong: the standard markdown-engine convention (used by GitHub-flavoured Markdown, Sphinx, MkDocs, pandoc, every modern markdown renderer in production) is to slugify the heading TEXT for the ID — <h1 id="introduction">Introduction</h1> — with a numeric suffix appended only when slug collisions occur. mistune's default punts the slugification entirely and produces purely positional IDs that an attacker can predict in O(1).

The downstream impacts: - Same-page links with click go to whichever element with id="toc1" appears first in tree order. If the attacker can land any HTML element with id="toc1" before the real heading (via inlinehtml with escape=False, via the include-directive HTML branch, via attacker-supplied content earlier in the document), navigation is hijacked. - CSS rules targeting #toc1 apply to the wrong element. - JavaScript bound to document.getElementById('toc1') operates on the wrong element. - The TOC's own <a href="#toc1"> link in the rendered TOC list points to whichever element wins the duplicate-ID race.

Exploit Chain

1. Application uses mistune with addtochook(md) or TableOfContents directive enabled (the documented setup for sites with TOC support). 2. Application renders an attacker-supplied document, or splices attacker content into a trusted document. With escape=False (or via the include-directive .html branch covered by my prior advisory), the attacker can place <a id="toc1">...</a> anywhere in the document. 3. mistune assigns id="toc1" to the first heading. Now there are two elements with id="toc1" in the page. 4. The rendered TOC contains <a href="#toc1">First heading</a>. Clicking it navigates to whichever element with id="toc1" appears first in tree order. If the attacker placed their <a id="toc1"> BEFORE the heading, navigation is hijacked. 5. Same-page CSS / JS / aria-described references to #toc1 similarly redirect.

Security Impact

Severity: sec-low. Not a direct XSS or RCE; the issue is identifier confusion that enables UI-redirection / navigation-hijack attacks. The realistic attacker capability is "make an internal anchor link go to attacker content instead of the real heading", or "make a CSS selector apply to attacker content", or "break aria/screen-reader associations". Attacker capability: with the ability to plant any HTML element with id="tocN" in the document, hijack <a href="#tocN"> navigation and any CSS/JS targeting that ID. With escape=False, this is straightforward. With escape=True, the attacker needs another vector to land a raw id attribute (one of the include-directive branches, a sibling tooling pipeline that lets HTML through, etc.). Preconditions: application uses TOC + the default headingid callback. If the application provides its own headingid (e.g., one based on slugified heading text, with collision suffixes), this finding does not apply. Differential: PoC-verified against mistune@3.2.1:

python import mistune from mistune.directives import RSTDirective, TableOfContents md = mistune.createmarkdown(plugins=[RSTDirective([TableOfContents()])])

print(md(''' .. toc::

Heading 1

Heading 2 '''))

Output (note: id="toc1" / id="toc2", purely positional): <details class="toc" open> <summary>Table of Contents</summary> <ul> <li><a href="#toc1">Heading 1</a></li> <li><a href="#toc2">Heading 2</a></li> </ul> </details> <h1 id="toc1">Heading 1</h1> <h1 id="toc2">Heading 2</h1>

The patched build (with the suggested fix below) produces text-derived slugs like id="heading-1" and id="heading-2", which are tied to content rather than position.

Suggested Fix

Default to slugifying the heading text:

diff --- a/src/mistune/toc.py +++ b/src/mistune/toc.py @@ -33,9 +33,18 @@ def addtochook(md, minlevel=1, maxlevel=3, headingid=None): if headingid is None: + import re + slugre = re.compile(r"[^a-z0-9]+") + seen = {} def headingid(token, index): - return "toc" + str(index + 1) + text = striptags(md.renderer(md.inline(token["text"], {}), BlockState())) + slug = slugre.sub("-", text.lower()).strip("-") or "section" + n = seen.get(slug, 0) + seen[slug] = n + 1 + return slug if n == 0 else f"{slug}-{n}"

Same change applies to src/mistune/directives/toc.py:33. Existing applications that have hardcoded #tocN anchors will break; document the migration in the changelog and consider providing an opt-out flag for the legacy behaviour. Add a regression test that asserts heading IDs are slug-derived, not position-derived, and that collisions get a -N suffix.

Other sources

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 tocN values without slugifying the heading text, allowing attacker-controlled id="tocN" 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.

MITRE

Affected Software

3 affected componentsFixes available
Mistune Mistune<3.3.0
Mistune Project Mistune<3.3.0
pip/mistune<3.3.0
3.3.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/mistune to a version that resolves this vulnerability.

    Fixed in 3.3.0
  2. Upgrade

    Upgrade mistune to a version that resolves this vulnerability.

    Fixed in 3.3.0

Event History

Jul 8, 2026
CVE Published
via MITRE·04:13 PM
Data Sourced
via MITRE·04:13 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·05:17 PM
RemedyDescriptionSeverityWeaknessAffected Software
Jul 20, 2026
Advisory Published
via GitHub·09:35 PM
Data Sourced
via GitHub·09:35 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-59930?

CVE-2026-59930 has a severity rating of medium with a score of 4.3.

2

How do I fix CVE-2026-59930?

To fix CVE-2026-59930, upgrade to Mistune version 3.3.0 or later.

3

What is the risk of CVE-2026-59930?

CVE-2026-59930 poses a risk of 22, indicating a moderate level of threat.

4

Which software is affected by CVE-2026-59930?

CVE-2026-59930 affects the Mistune Markdown parser, specifically versions prior to 3.3.0.

5

What type of attack does CVE-2026-59930 allow?

CVE-2026-59930 allows for potential collision attacks with attacker-controlled `id="toc_N"` content.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203