Skip to main content

pymdown-extensions CVE-2026-46338

| EUVDEUVD-2026-45003 MEDIUM
Path Traversal (CWE-22)
2026-05-19 https://github.com/facelessuser/pymdown-extensions GHSA-62q4-447f-wv8h PYSEC-2026-2999
4.3
CVSS 3.1 · Vendor: https://github.com/facelessuser/pymdown-extensions
Share

Severity by source

Vendor (https://github.com/facelessuser/pymdown-extensions) PRIMARY
4.3 MEDIUM
AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N

Primary rating from Vendor (https://github.com/facelessuser/pymdown-extensions) · only source for this CVE.

CVSS VectorVendor: https://github.com/facelessuser/pymdown-extensions

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

Lifecycle Timeline

2
Source Code Evidence Fetched
May 19, 2026 - 20:17 vuln.today
Analysis Generated
May 19, 2026 - 20:17 vuln.today

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 6 pypi packages depend on pymdown-extensions (5 direct, 1 indirect)

Ecosystem-wide dependent count for version 10.0.1.

DescriptionCVE.org

Summary

pymdownx.snippets has a regression of the CVE-2023-32309 / GHSA-jh85-wwv9-24hv fix. With restrict_base_path: True (the default), the current filename.startswith(base) containment check does not enforce a directory boundary. As a result, a markdown snippet directive can read files from sibling paths that share the same prefix as base_path, such as docs vs docs_internal.

The regression was introduced in PR #2039 / commit 7c13bda5b7793b172efd1abb6712e156a83fe07d, which replaced the original directory-identity check with a plain string-prefix comparison.

Details

The regression was introduced in commit 7c13bda5b7793b172efd1abb6712e156a83fe07d (2023-05-15, #2039 *"Fix regression of snippets nested deeply under specified base path"*), which relaxed the original os.path.samefile(base, os.path.dirname(filename)) check to a plain startswith(base).

SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py:

python
if self.restrict_base_path:
    filename = os.path.abspath(os.path.join(base, path))
# If the absolute path is no longer under the specified base path, reject the file
    if not filename.startswith(base):
        continue

base is os.path.abspath(b) and has no trailing separator. str.startswith(base) is True for any filename whose string representation begins with the same characters as base, regardless of whether those characters end at a directory boundary.

Concrete example:

  • base = "/x/docs"
  • path = "../docs_secret/leak.txt" (inside the markdown snippet directive)
  • os.path.join(base, path)"/x/docs/../docs_secret/leak.txt"
  • os.path.abspath(...)"/x/docs_secret/leak.txt"
  • filename.startswith(base)True, because "/x/docs_secret/..." begins with the literal string "/x/docs".

All releases from 10.0.1 (2023-05-15) through 10.21.2 (current) are affected.

Impact

Arbitrary file read within the host the build runs on, bounded by the prefix match. With base_path = /x/docs the attacker can read files from any sibling directory whose path begins with the literal string /x/docs followed by any non-separator character - for example /x/docs_internal/, /x/docs.bak/, /x/docs2/.

The threat model is the same as the original CVE-2023-32309: markdown content processed by the snippets preprocessor in a build pipeline (typical scenario: an MkDocs documentation site built in CI from PR contributions or otherwise less-trusted markdown) can read files outside the configured base. CI builds that publish the generated HTML expose the read file to the public; CI builds with secrets on disk leak those secrets.

Reproduction

Minimal local PoC, non-destructive:

python
import os, shutil, tempfile, markdown

work = tempfile.mkdtemp(prefix="pmx_poc_")
try:
    base    = os.path.join(work, "docs")
    sibling = os.path.join(work, "docs_secret")
    os.makedirs(base)
    os.makedirs(sibling)
    with open(os.path.join(sibling, "leak.txt"), "w") as f:
        f.write("TOP_SECRET_FROM_SIBLING_DIR\n")

    out = markdown.markdown(
        '--8<-- "../docs_secret/leak.txt"\n',
        extensions=["pymdownx.snippets"],
        extension_configs={
            "pymdownx.snippets": {
                "base_path": [base],
                "restrict_base_path": True,
                "check_paths": True,
            }
        },
    )
    print(out)
# -> <p>TOP_SECRET_FROM_SIBLING_DIR</p>
finally:
    shutil.rmtree(work)

Default restrict_base_path: True is sufficient - no non-default option is required.

Suggested fix

Minimal change - require the separator after the base prefix:

diff
-                        if not filename.startswith(base):
+
# Append `os.sep` so a sibling directory whose name shares a prefix
+
# (e.g. `/x/docs` vs `/x/docs_evil`) cannot satisfy the check.
+                        if not filename.startswith(base + os.sep):
                             continue

This preserves the original intent (allow snippets nested at any depth under base_path) while restoring the directory-boundary check. It does not affect the os.path.isdir(base) branch where base is a file (that branch still uses os.path.samefile).

Alternative: os.path.commonpath([base, filename]) == base is equivalent and slightly more idiomatic, though it raises ValueError on different drives on Windows and would need a try/except. The startswith(base + os.sep) fix is the smaller diff.

Note: this fix does not change behaviour for symlinks inside base_path. The existing implementation uses os.path.abspath (not os.path.realpath), so a symlink within base_path pointing outside is still followed. That is a separate concern - symlinks require write access to base_path, a much higher bar than the current bypass - and matches the behaviour the CVE-2023 fix established.

Regression test

A regression test class TestSnippetsSiblingPrefix was added in tests/test_extensions/test_snippets.py. It uses tests/test_extensions/_snippets/nested as base_path and a new fixture directory tests/test_extensions/_snippets/nested_sibling_evil/leak.txt. It asserts that the markdown directive --8<-- "../nested_sibling_evil/leak.txt" raises SnippetMissingError.

  • Without fix: test fails (AssertionError: SnippetMissingError not raised, sibling file is silently read).
  • With fix: test passes.

Full suite: python -m pytest tests/ -q738 passed (737 baseline + 1 new regression test). No regressions.

Affected versions

>= 10.0.1, <= 10.21.2

AnalysisAI

Path traversal in pymdownx.snippets versions 10.0.1 through 10.21.2 allows unauthenticated remote attackers to read arbitrary files from sibling directories outside the configured base_path, bypassing the restrict_base_path protection intended by CVE-2023-32309. The bypass exploits a string-prefix comparison introduced in PR #2039 that lacks directory-boundary enforcement, enabling a crafted snippet directive like '--8<-- "../docs_secret/leak.txt"' to escape the configured base directory when sibling paths share the same string prefix. Publicly available exploit code (proof-of-concept) exists in the GitHub Security Advisory; the vulnerability is not confirmed actively exploited in the CISA KEV catalog at time of analysis.

Technical ContextAI

The vulnerability resides in SnippetPreprocessor.get_snippet_path() within pymdownx/snippets.py (package: pkg:pip/pymdown-extensions). The root cause class is CWE-22 (Improper Limitation of a Pathname to a Restricted Directory). The original CVE-2023-32309 fix used os.path.samefile(base, os.path.dirname(filename)) to verify directory identity, but PR #2039 (commit 7c13bda5b7793b172efd1abb6712e156a83fe07d, 2023-05-15) replaced this with filename.startswith(base), where base is os.path.abspath(b) with no trailing separator. Because Python's str.startswith() performs a raw character comparison, a resolved path like /x/docs_secret/leak.txt satisfies startswith('/x/docs') even though /x/docs_secret is a sibling directory, not a subdirectory. The Markdown Snippet extension processes '--8<-- path' directives at build time, so any markdown pipeline using pymdownx.snippets with restrict_base_path: True (the default) is affected on any platform where sibling paths with shared prefixes exist.

RemediationAI

Upgrade pymdown-extensions to version 10.21.3, which is confirmed as the fixed release per the GHSA-62q4-447f-wv8h advisory (https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-62q4-447f-wv8h). The fix changes the containment check in pymdownx/snippets.py from filename.startswith(base) to filename.startswith(base + os.sep), restoring directory-boundary enforcement while preserving support for snippets nested at arbitrary depth under base_path. If an immediate upgrade is not possible, compensating controls include: (1) restricting who can submit or modify markdown files processed by the snippets extension in CI - this eliminates untrusted-contributor access but requires strict branch protection; (2) ensuring no sensitive secrets or files reside in any directory whose name shares a string prefix with the configured base_path on the build host - this limits the blast radius but is operationally fragile; (3) running the documentation build in an isolated container or sandbox where the filesystem contains only the docs directory tree, preventing sibling paths from existing - this is the most reliable workaround and has no functional trade-offs beyond added build infrastructure complexity. Do not rely on setting restrict_base_path: True as a security control until upgraded, as that setting is vulnerable by design in affected versions.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-46338 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy