CSIRTS // UNIFIED SECURITY ADVISORY FEEDSYS ● ONLINE · POWERED BY INTELFUSIONS.COM

GHSA-8mpj-m6qm-5qr8: Mistune directives/include: mutual `.. include::` recursion crashes the renderer with `RecursionError`, denial of service via two attacker-controlled markdown files

mediumCVSS 5.3CVE-2026-59927
Summary Type: Uncontrolled recursion via mutual include. The Include directive checks for direct self-reference (a.md cannot include a.md), but does not detect indirect cycles. Two markdown files that include each other (a.md → includes b.md → includes a.md) cause unbounded recursion until Python's stack limit fires RecursionError. The exception propagates out of the renderer and crashes the calling code. File: src/mistune/directives/include.py, lines 33-37 (the self-include check is the only cycle-detection logic). Root cause: the include logic only compares os.path.abspath(dest) == os.path.abspath(source_file). There is no per-render set of "files already included" that would catch transitive cycles. When a.md includes b.md, the recursive block.parse(new_state) call uses dest (b.md) as the new file, which then includes a.md (passing the self-check, because the immediate parent file is b.md, not a.md), which then includes b.md, and so on. Each recursion level adds Python frames; the default stack limit of 1000 frames trips after ~7-10 cycle iterations and Python raises RecursionError. Since the directive does not catch the exception, it propagates out of Markdown.parse() and surfaces in the calling code, crashing the request. Affected Code File: src/mistune/directives/include.py, lines 28-54. relpath = self.parse_title(m) dest = os.path.join(os.path.dirname(source_file), relpath) dest = os.path.normpath(dest) if os.path.abspath(dest) == os.path.abspath(source_file): # <-- only catches direct self-include return {"type": "block_error", "raw": "Could not include self: " + relpath} if not os.path.isfile(dest): return {"type": "block_error", "raw": "Could not find file: " + relpath} with open(dest, "rb") as f: content = f.read().decode(encoding) ext = os.path.splitext(relpath)[1] if ext in {".md", ".markdown", ".mkd"}: new_state = block.state_cls() new_state.env["file"] = dest new_state.process(content) block.parse(new_state) # <-- recursive parse, no cycle tra

Details

Source
GitHub Security Advisories (INTL · database · site)
Severity
medium — CVSS 5.3
Published
2026-07-20
Last updated
2026-07-20
Exploitation
Not in CISA KEV at last sync

Original advisory: https://github.com/advisories/GHSA-8mpj-m6qm-5qr8

Exploitation outlook

EPSS (FIRST.org) estimates each CVE’s probability of exploitation in the next 30 days — here is the CSIRTS.com read on those numbers.

Referenced CVEs

CVECSIRTS overviewExternal
CVE-2026-59927coverage & exploitation statusNVD · CVE.org

Same CVEs, other sources

How other CERTs, PSIRTs and databases cover the vulnerabilities in this advisory.

More from GitHub Security Advisories