GHSA-8mpj-m6qm-5qr8: Mistune directives/include: mutual `.. include::` recursion crashes the renderer with `RecursionError`, denial of service via two attacker-controlled markdown files
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
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.
- Low exploitation riskCVE-2026-599270.35% 30-day exploitation probability — currently an unlikely target, but scores change as exploit code circulates. Riskier than 28% of all scored CVEs.
Referenced CVEs
| CVE | CSIRTS overview | External |
|---|---|---|
| CVE-2026-59927 | coverage & exploitation status | NVD · CVE.org |
Same CVEs, other sources
How other CERTs, PSIRTs and databases cover the vulnerabilities in this advisory.
More from GitHub Security Advisories
- mediumGHSA-jr6p-8pjj-mfx6: Capsule has an incomplete fix of CVE-2026-22872: TenantResource RawItems and Generators s…2026-07-31
- mediumGHSA-68cj-mvg9-rgm2: Capsule: CapsuleConfiguration NodeMetadata regex fields lack webhook validation, allowing…2026-07-31
- mediumGHSA-ff84-5f28-78qj: re2: Out-of-bounds heap read in `exec`/`test`/`match` via attacker-influenced `lastIndex`…2026-07-31
- mediumGHSA-6hxr-mr5r-9836: re2: Global `String.prototype.match` with an empty-matchable pattern never advances → inf…2026-07-31
- mediumGHSA-x83g-979r-f5fh: Sylius Mollie Plugin has unauthenticated IDOR that leaks order token and customer PII2026-07-31