CVE-2026-59927
Unbounded recursion in Mistune's include directive can cause a DoS.
- CVSS 5.3
- CWE-674
- Resource Management
- Remote
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.3.0, the Include directive in src/mistune/directives/include.py detects only direct self-includes and not indirect cycles, allowing two markdown files that include each other to trigger unbounded recursion, raise RecursionError, and crash the rendering request. This issue is fixed in version 3.3.0.
- CWE
- CWE-674
- CVSS base score
- 5.3
- Published
- 2026-07-08
- OWASP
- A04 Insecure Design
- Orthogonal defect classification
- Algorithm
- Code defect classification
- Missing Check
- Category
- Resource Management
- Subcategory
- Resource Exhaustion
- Accessibility scope
- Remote
- Impact
- Denial of Service (DoS)
- Affected component
- Mistune
- Fixed by upgrading
- Yes
Solution
Upgrade Mistune to version 3.3.0 or later.
Vulnerable code sample
import mistune
from mistune.directives.include import Include
import os
# NOTE: This code requires a vulnerable version of mistune (< 3.3.0).
# For example: pip install "mistune<3.3.0"
# 1. Create two markdown files that form an indirect inclusion cycle.
with open("file_a.md", "w") as f:
f.write("This is file A, which includes file B.\n")
f.write(".. include:: file_b.md\n")
with open("file_b.md", "w") as f:
f.write("This is file B, which includes file A.\n")
f.write(".. include:: file_a.md\n")
# 2. Set up the mistune parser with the 'Include' directive.
markdown = mistune.create_markdown(plugins=[Include()])
# 3. Read the content of the starting file.
with open("file_a.md", "r") as f:
content = f.read()
# 4. Trigger the vulnerability.
# The 'state' dictionary is necessary to provide context (like the current file's path)
# to the include directive.
# In a vulnerable version, this call will enter an infinite loop of A including B
# and B including A, eventually raising a RecursionError and crashing the process.
try:
state = {'filepath': os.path.abspath('file_a.md')}
markdown(content, state)
except RecursionError:
print("Vulnerability triggered: Caught expected RecursionError.")
finally:
# 5. Clean up the created files.
os.remove("file_a.md")
os.remove("file_b.md")Patched code sample
This code example conceptually represents the logic used to fix the unbounded recursion vulnerability in Mistune. The actual implementation is part of the `Include` directive class. The fix involves tracking the history of included files during a single rendering pass to detect indirect cycles.
```python
class FixedIncludeDirective:
"""
A conceptual representation of the fixed Mistune Include directive logic.
This would be part of a larger Markdown parsing system.
"""
def __call__(self, directive, md):
"""
This method is called when an '!include' directive is processed.
:param directive: An object containing parsed directive info,
including the filename in `directive.text`.
:param md: The main Markdown parser instance, which holds the
rendering 'state'.
"""
# The 'state' object is used to pass information through a single
# rendering process. We use its 'env' dictionary to store our history.
state = md.state
# Initialize the include history set if it doesn't exist for this render.
if '_include_hist' not in state.env:
state.env['_include_hist'] = set()
filepath = directive.text.strip()
# --- THE FIX ---
# 1. Check for circular inclusion. Before including the new file,
# check if its path is already in the history for the current
# rendering chain.
if filepath in state.env['_include_hist']:
# If it is, an indirect cycle has been detected. Raise an error
# to halt execution and prevent infinite recursion.
raise RecursionError(f"Circular inclusion detected: {filepath}")
# 2. Add the current file to the history before processing it.
state.env['_include_hist'].add(filepath)
# In the actual library, this section reads the file content
# and recursively calls the parser on that content. For example:
#
# with open(filepath, 'r') as f:
# content = f.read()
# md.parse(content)
# 3. After the file (and any of its own nested includes) is fully
# processed, remove it from the history. This is crucial because
# it allows the same file to be included again later in a
# different, non-circular part of the document tree.
state.env['_include_hist'].remove(filepath)
# The directive returns the rendered content (which is empty in this case,
# as the included content is parsed directly into the main document).
return ''Payload
.. include:: file_b.md
```
```markdown
.. include:: file_a.md
Cite this entry
@misc{vaitp:cve202659927,
title = {{Unbounded recursion in Mistune's include directive can cause a DoS.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-59927},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59927/}}
}
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 ::
