{"id":"CVE-2026-46338","aliases":["DEBIAN-CVE-2026-46338","EUVD-2026-45003","GHSA-62q4-447f-wv8h","OESA-2026-4182","OESA-2026-4183","OESA-2026-4184","PYSEC-2026-2999","ROOT-APP-PYPI-CVE-2026-46338","UBUNTU-CVE-2026-46338"],"status":"published","title":"PyMdown Extensions: Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","severity":{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00398,"percentile":0.34323,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22"],"published":"2026-05-19T20:00:29Z","modified":"2026-10-28T02:30:53Z","fixes":[{"ecosystem":"PyPI","package":"pymdown-extensions","fixed_in":["10.21.3"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"pymdown-extensions","fixed_in":["11.0.1-1"],"sources":["osv"]},{"ecosystem":"Root:PyPI","package":"pymdown-extensions","fixed_in":["10.21.2+aikido.2","10.21.2+aikido.1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"10.0.1"},{"last_affected":"10.21.2"},{"fixed":"10.21.3"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.11.2-1","10.13-1","10.13-2","10.13-3","10.13-4","10.19.1-1","10.19.1-3","10.8.1-1","10.9-1","11.0.1-1","9.5-2","9.5-3","9.5-4","9.5-5"]},{"source":"osv","ecosystem":"Debian:13","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.13-1","10.13-1+deb13u1","10.13-2","10.13-3","10.13-4","10.19.1-1","10.19.1-3","11.0.1-1"]},{"source":"osv","ecosystem":"Debian:14","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"11.0.1-1"}]}],"versions":["10.13-1","10.13-2","10.13-3","10.13-4","10.19.1-1","10.19.1-3"]},{"source":"osv","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"10.0.1"},{"fixed":"10.21.3"}]}],"versions":["10.0.1","10.1","10.10","10.10.1","10.10.2","10.11","10.11.1","10.11.2","10.12","10.13","10.14","10.14.1","10.14.2","10.14.3","10.15","10.16","10.16.1","10.17","10.17.1","10.17.2","10.18","10.19","10.19.1","10.2","10.2.1","10.20","10.20.1","10.21","10.21.2","10.3","10.3.1","10.4","10.5","10.6","10.7","10.7.1","10.8","10.8.1","10.9"]},{"source":"osv","ecosystem":"openEuler:24.03-LTS-SP4","package":"python-pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.7-3.oe2403sp4"}]}],"bundled":3},{"source":"osv","ecosystem":"openEuler:24.03-LTS-SP1","package":"python-pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.7-3.oe2403sp1"}]}],"bundled":3},{"source":"osv","ecosystem":"openEuler:24.03-LTS-SP3","package":"python-pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.7-3.oe2403sp3"}]}],"bundled":3},{"source":"osv","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"10.0.1"},{"fixed":"10.21.3"}]}],"versions":["10.0.1","10.1","10.10","10.10.1","10.10.2","10.11","10.11.1","10.11.2","10.12","10.13","10.14","10.14.1","10.14.2","10.14.3","10.15","10.16","10.16.1","10.17","10.17.1","10.17.2","10.18","10.19","10.19.1","10.2","10.2.1","10.20","10.20.1","10.21","10.21.2","10.3","10.3.1","10.4","10.5","10.6","10.7","10.7.1","10.8","10.8.1","10.9"]},{"source":"osv","ecosystem":"Root:PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.21.2+aikido.2"},{"fixed":"10.21.2+aikido.1"}]}]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["9.5-2","9.5-4","9.5-5"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.19.1-3","11.0.1-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.13-3","10.13-4","10.19.1-3"]}],"products":[{"vendor":"facelessuser","product":"pymdown-extensions"},{"vendor":"facelessuser","product":"pymdown_extensions"}],"references":[{"url":"https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-62q4-447f-wv8h","tags":["x_refsource_CONFIRM","exploit","Exploit","Mitigation","Vendor Advisory","advisory","web","report"]},{"url":"https://github.com/facelessuser/pymdown-extensions/commit/63b7835776d703d6c339cf2110d9888f676efc0c","tags":["x_refsource_MISC","Patch","fix","report"]},{"url":"https://github.com/facelessuser/pymdown-extensions/releases/tag/10.21.3","tags":["x_refsource_MISC","Product","Release Notes","web","report"]},{"url":"https://github.com/facelessuser/pymdown-extensions/pull/2039","tags":["web"]},{"url":"https://github.com/advisories/GHSA-62q4-447f-wv8h","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46338.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46338","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-46338","tags":["advisory"]},{"url":"https://github.com/facelessuser/pymdown-extensions","tags":["package"]},{"url":"https://www.openeuler.org/zh/security/security-bulletins/detail/?id=openEuler-SA-2026-4182","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61632","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67422","tags":["advisory"]},{"url":"https://www.openeuler.org/zh/security/security-bulletins/detail/?id=openEuler-SA-2026-4183","tags":["advisory"]},{"url":"https://www.openeuler.org/zh/security/security-bulletins/detail/?id=openEuler-SA-2026-4184","tags":["advisory"]},{"url":"https://pypi.org/project/pymdown-extensions","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-46338","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-46338","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-46338","title":"PyMdown Extensions: Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","status":"published","published":"2026-07-16T18:24:18.155Z","modified":"2026-07-17T17:52:57.729Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T13:47:06.353366Z"}],"euvd":[{"id":"EUVD-2026-45003","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","status":"published","published":"2026-07-16T18:24:18Z","modified":"2026-07-17T17:52:57Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T18:19:50.451546Z"}],"ghsa":[{"id":"GHSA-62q4-447f-wv8h","title":"Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path","description":"# Summary\n\n`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`.\n\nThe regression was introduced in PR #2039 / commit `7c13bda5b7793b172efd1abb6712e156a83fe07d`, which replaced the original directory-identity check with a plain string-prefix comparison.\n\n# Details\n\nThe 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)`.\n\n`SnippetPreprocessor.get_snippet_path()` in `pymdownx/snippets.py`:\n\n```python\nif self.restrict_base_path:\n    filename = os.path.abspath(os.path.join(base, path))\n    # If the absolute path is no longer under the specified base path, reject the file\n    if not filename.startswith(base):\n        continue\n```\n\n`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.\n\nConcrete example:\n\n* `base = \"/x/docs\"`\n* `path = \"../docs_secret/leak.txt\"` (inside the markdown snippet directive)\n* `os.path.join(base, path)` → `\"/x/docs/../docs_secret/leak.txt\"`\n* `os.path.abspath(...)`     → `\"/x/docs_secret/leak.txt\"`\n* `filename.startswith(base)` → `True`, because `\"/x/docs_secret/...\"` begins with the literal string `\"/x/docs\"`.\n\nAll releases from **10.0.1 (2023-05-15) through 10.21.2 (current)** are affected.\n\n# Impact\n\nArbitrary 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/`.\n\nThe 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.\n\n# Reproduction\n\nMinimal local PoC, non-destructive:\n\n```python\nimport os, shutil, tempfile, markdown\n\nwork = tempfile.mkdtemp(prefix=\"pmx_poc_\")\ntry:\n    base    = os.path.join(work, \"docs\")\n    sibling = os.path.join(work, \"docs_secret\")\n    os.makedirs(base)\n    os.makedirs(sibling)\n    with open(os.path.join(sibling, \"leak.txt\"), \"w\") as f:\n        f.write(\"TOP_SECRET_FROM_SIBLING_DIR\\n\")\n\n    out = markdown.markdown(\n        '--8\u003c-- \"../docs_secret/leak.txt\"\\n',\n        extensions=[\"pymdownx.snippets\"],\n        extension_configs={\n            \"pymdownx.snippets\": {\n                \"base_path\": [base],\n                \"restrict_base_path\": True,\n                \"check_paths\": True,\n            }\n        },\n    )\n    print(out)  # -\u003e \u003cp\u003eTOP_SECRET_FROM_SIBLING_DIR\u003c/p\u003e\nfinally:\n    shutil.rmtree(work)\n```\n\nDefault `restrict_base_path: True` is sufficient — no non-default option is required.\n\n# Suggested fix\n\nMinimal change — require the separator after the base prefix:\n\n```diff\n-                        if not filename.startswith(base):\n+                        # Append `os.sep` so a sibling directory whose name shares a prefix\n+                        # (e.g. `/x/docs` vs `/x/docs_evil`) cannot satisfy the check.\n+                        if not filename.startswith(base + os.sep):\n                             continue\n```\n\nThis 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`).\n\nAlternative: `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.\n\nNote: 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.\n\n# Regression test\n\nA 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\u003c-- \"../nested_sibling_evil/leak.txt\"` raises `SnippetMissingError`.\n\n* Without fix: test fails (`AssertionError: SnippetMissingError not raised`, sibling file is silently read).\n* With fix: test passes.\n\nFull suite: `python -m pytest tests/ -q` → **738 passed** (737 baseline + 1 new regression test). No regressions.\n\n# Affected versions\n\n`\u003e= 10.0.1, \u003c= 10.21.2`","status":"published","published":"2026-05-19T20:00:29Z","modified":"2026-05-19T20:00:30Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:40:12.430932Z"}],"nvd":[{"id":"CVE-2026-46338","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","status":"published","published":"2026-07-16T19:16:45.733Z","modified":"2026-10-24T20:10:00.137Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:12:30.250437Z"}],"osv":[{"id":"CVE-2026-46338","title":"PyMdown Extensions: Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","status":"published","published":"2026-07-16T18:24:18.155Z","modified":"2026-10-28T02:30:53.694392378Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:45:05.870292Z"},{"id":"DEBIAN-CVE-2026-46338","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","status":"published","published":"2026-07-16T19:16:45.733Z","modified":"2026-10-25T09:47:30.510417473Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T15:45:56.025495Z"},{"id":"GHSA-62q4-447f-wv8h","title":"Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path","description":"# Summary\n\n`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`.\n\nThe regression was introduced in PR #2039 / commit `7c13bda5b7793b172efd1abb6712e156a83fe07d`, which replaced the original directory-identity check with a plain string-prefix comparison.\n\n# Details\n\nThe 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)`.\n\n`SnippetPreprocessor.get_snippet_path()` in `pymdownx/snippets.py`:\n\n```python\nif self.restrict_base_path:\n    filename = os.path.abspath(os.path.join(base, path))\n    # If the absolute path is no longer under the specified base path, reject the file\n    if not filename.startswith(base):\n        continue\n```\n\n`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.\n\nConcrete example:\n\n* `base = \"/x/docs\"`\n* `path = \"../docs_secret/leak.txt\"` (inside the markdown snippet directive)\n* `os.path.join(base, path)` → `\"/x/docs/../docs_secret/leak.txt\"`\n* `os.path.abspath(...)`     → `\"/x/docs_secret/leak.txt\"`\n* `filename.startswith(base)` → `True`, because `\"/x/docs_secret/...\"` begins with the literal string `\"/x/docs\"`.\n\nAll releases from **10.0.1 (2023-05-15) through 10.21.2 (current)** are affected.\n\n# Impact\n\nArbitrary 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/`.\n\nThe 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.\n\n# Reproduction\n\nMinimal local PoC, non-destructive:\n\n```python\nimport os, shutil, tempfile, markdown\n\nwork = tempfile.mkdtemp(prefix=\"pmx_poc_\")\ntry:\n    base    = os.path.join(work, \"docs\")\n    sibling = os.path.join(work, \"docs_secret\")\n    os.makedirs(base)\n    os.makedirs(sibling)\n    with open(os.path.join(sibling, \"leak.txt\"), \"w\") as f:\n        f.write(\"TOP_SECRET_FROM_SIBLING_DIR\\n\")\n\n    out = markdown.markdown(\n        '--8\u003c-- \"../docs_secret/leak.txt\"\\n',\n        extensions=[\"pymdownx.snippets\"],\n        extension_configs={\n            \"pymdownx.snippets\": {\n                \"base_path\": [base],\n                \"restrict_base_path\": True,\n                \"check_paths\": True,\n            }\n        },\n    )\n    print(out)  # -\u003e \u003cp\u003eTOP_SECRET_FROM_SIBLING_DIR\u003c/p\u003e\nfinally:\n    shutil.rmtree(work)\n```\n\nDefault `restrict_base_path: True` is sufficient — no non-default option is required.\n\n# Suggested fix\n\nMinimal change — require the separator after the base prefix:\n\n```diff\n-                        if not filename.startswith(base):\n+                        # Append `os.sep` so a sibling directory whose name shares a prefix\n+                        # (e.g. `/x/docs` vs `/x/docs_evil`) cannot satisfy the check.\n+                        if not filename.startswith(base + os.sep):\n                             continue\n```\n\nThis 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`).\n\nAlternative: `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.\n\nNote: 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.\n\n# Regression test\n\nA 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\u003c-- \"../nested_sibling_evil/leak.txt\"` raises `SnippetMissingError`.\n\n* Without fix: test fails (`AssertionError: SnippetMissingError not raised`, sibling file is silently read).\n* With fix: test passes.\n\nFull suite: `python -m pytest tests/ -q` → **738 passed** (737 baseline + 1 new regression test). No regressions.\n\n# Affected versions\n\n`\u003e= 10.0.1, \u003c= 10.21.2`","status":"published","published":"2026-05-19T20:00:29Z","modified":"2026-09-10T03:50:45.703936995Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:38.911014Z"},{"id":"OESA-2026-4182","title":"python-pymdown-extensions security update","description":"Extension pack for Python Markdown.\r\n\r\nSecurity Fix(es):\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.(CVE-2026-46338)\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. In versions up to and including 10.21.3, the b64 extension is vulnerable to a path traversal that discloses arbitrary files: it inlines images referenced by \u0026lt;img src=\u0026quot;...\u0026quot;\u0026gt; by joining the src onto the configured base_path with os.path.normpath and opening the result directly, without verifying that the resolved path stays inside base_path. As a result, an src containing ../ sequences or an absolute path reads a file outside base_path as long as it has an allowed image extension (.png, .jpg, .jpeg, .gif, .svg), and the file\u0026apos;s contents are then base64-encoded into the rendered output, disclosing them. An application that renders untrusted Markdown with pymdownx.b64 enabled can therefore leak the contents of image-extension files readable by the process to whoever controls the Markdown or views the output, a targeted file-read bounded by the extension check. This issue has been fixed in version 11.0.(CVE-2026-61632)\n\npymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension\u0026apos;s default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.(CVE-2026-67422)","status":"published","published":"2026-09-30T13:47:22Z","modified":"2026-10-01T02:00:07.617204605Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:47.643553Z"},{"id":"OESA-2026-4183","title":"python-pymdown-extensions security update","description":"Extension pack for Python Markdown.\r\n\r\nSecurity Fix(es):\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.(CVE-2026-46338)\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. In versions up to and including 10.21.3, the b64 extension is vulnerable to a path traversal that discloses arbitrary files: it inlines images referenced by \u0026lt;img src=\u0026quot;...\u0026quot;\u0026gt; by joining the src onto the configured base_path with os.path.normpath and opening the result directly, without verifying that the resolved path stays inside base_path. As a result, an src containing ../ sequences or an absolute path reads a file outside base_path as long as it has an allowed image extension (.png, .jpg, .jpeg, .gif, .svg), and the file\u0026apos;s contents are then base64-encoded into the rendered output, disclosing them. An application that renders untrusted Markdown with pymdownx.b64 enabled can therefore leak the contents of image-extension files readable by the process to whoever controls the Markdown or views the output, a targeted file-read bounded by the extension check. This issue has been fixed in version 11.0.(CVE-2026-61632)\n\npymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension\u0026apos;s default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.(CVE-2026-67422)","status":"published","published":"2026-09-30T13:47:23Z","modified":"2026-10-01T02:00:06.915388189Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:47.643553Z"},{"id":"OESA-2026-4184","title":"python-pymdown-extensions security update","description":"Extension pack for Python Markdown.\r\n\r\nSecurity Fix(es):\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.(CVE-2026-46338)\n\nPyMdown Extensions is a set of extensions for the Python-Markdown markdown project. In versions up to and including 10.21.3, the b64 extension is vulnerable to a path traversal that discloses arbitrary files: it inlines images referenced by \u0026lt;img src=\u0026quot;...\u0026quot;\u0026gt; by joining the src onto the configured base_path with os.path.normpath and opening the result directly, without verifying that the resolved path stays inside base_path. As a result, an src containing ../ sequences or an absolute path reads a file outside base_path as long as it has an allowed image extension (.png, .jpg, .jpeg, .gif, .svg), and the file\u0026apos;s contents are then base64-encoded into the rendered output, disclosing them. An application that renders untrusted Markdown with pymdownx.b64 enabled can therefore leak the contents of image-extension files readable by the process to whoever controls the Markdown or views the output, a targeted file-read bounded by the extension check. This issue has been fixed in version 11.0.(CVE-2026-61632)\n\npymdown-extensions is a collection of extensions for the Python Markdown library. In versions up to and including 11.0, four inline processors (caret, tilde, betterem, and magiclink) use regular expressions whose content groups can partition a run of delimiter characters in exponentially many ways, causing catastrophic backtracking. As a result, a single untrusted Markdown line under 50 bytes rendered with markdown.markdown() in each extension\u0026apos;s default configuration drives the rendering thread into unbounded CPU usage that grows exponentially with input length, enabling an unauthenticated remote attacker who can submit Markdown to cause denial of service. The exposure is concrete for web applications that render user-supplied Markdown (comments, wikis, issue bodies, live preview), including any app using pymdownx.extra which bundles the vulnerable betterem default, as well as hosted docs/CI systems that build untrusted Markdown. The issue has been fixed in version 11.0.1.(CVE-2026-67422)","status":"published","published":"2026-09-30T13:47:23Z","modified":"2026-10-01T02:00:06.782014487Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:47.643553Z"},{"id":"PYSEC-2026-2999","title":"Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path","description":"# Summary\n\n`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`.\n\nThe regression was introduced in PR #2039 / commit `7c13bda5b7793b172efd1abb6712e156a83fe07d`, which replaced the original directory-identity check with a plain string-prefix comparison.\n\n# Details\n\nThe 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)`.\n\n`SnippetPreprocessor.get_snippet_path()` in `pymdownx/snippets.py`:\n\n```python\nif self.restrict_base_path:\n    filename = os.path.abspath(os.path.join(base, path))\n    # If the absolute path is no longer under the specified base path, reject the file\n    if not filename.startswith(base):\n        continue\n```\n\n`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.\n\nConcrete example:\n\n* `base = \"/x/docs\"`\n* `path = \"../docs_secret/leak.txt\"` (inside the markdown snippet directive)\n* `os.path.join(base, path)` → `\"/x/docs/../docs_secret/leak.txt\"`\n* `os.path.abspath(...)`     → `\"/x/docs_secret/leak.txt\"`\n* `filename.startswith(base)` → `True`, because `\"/x/docs_secret/...\"` begins with the literal string `\"/x/docs\"`.\n\nAll releases from **10.0.1 (2023-05-15) through 10.21.2 (current)** are affected.\n\n# Impact\n\nArbitrary 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/`.\n\nThe 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.\n\n# Reproduction\n\nMinimal local PoC, non-destructive:\n\n```python\nimport os, shutil, tempfile, markdown\n\nwork = tempfile.mkdtemp(prefix=\"pmx_poc_\")\ntry:\n    base    = os.path.join(work, \"docs\")\n    sibling = os.path.join(work, \"docs_secret\")\n    os.makedirs(base)\n    os.makedirs(sibling)\n    with open(os.path.join(sibling, \"leak.txt\"), \"w\") as f:\n        f.write(\"TOP_SECRET_FROM_SIBLING_DIR\\n\")\n\n    out = markdown.markdown(\n        '--8\u003c-- \"../docs_secret/leak.txt\"\\n',\n        extensions=[\"pymdownx.snippets\"],\n        extension_configs={\n            \"pymdownx.snippets\": {\n                \"base_path\": [base],\n                \"restrict_base_path\": True,\n                \"check_paths\": True,\n            }\n        },\n    )\n    print(out)  # -\u003e \u003cp\u003eTOP_SECRET_FROM_SIBLING_DIR\u003c/p\u003e\nfinally:\n    shutil.rmtree(work)\n```\n\nDefault `restrict_base_path: True` is sufficient — no non-default option is required.\n\n# Suggested fix\n\nMinimal change — require the separator after the base prefix:\n\n```diff\n-                        if not filename.startswith(base):\n+                        # Append `os.sep` so a sibling directory whose name shares a prefix\n+                        # (e.g. `/x/docs` vs `/x/docs_evil`) cannot satisfy the check.\n+                        if not filename.startswith(base + os.sep):\n                             continue\n```\n\nThis 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`).\n\nAlternative: `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.\n\nNote: 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.\n\n# Regression test\n\nA 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\u003c-- \"../nested_sibling_evil/leak.txt\"` raises `SnippetMissingError`.\n\n* Without fix: test fails (`AssertionError: SnippetMissingError not raised`, sibling file is silently read).\n* With fix: test passes.\n\nFull suite: `python -m pytest tests/ -q` → **738 passed** (737 baseline + 1 new regression test). No regressions.\n\n# Affected versions\n\n`\u003e= 10.0.1, \u003c= 10.21.2`","status":"published","published":"2026-07-13T15:19:09.231055Z","modified":"2026-07-13T16:33:07.759730233Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T15:52:56.429374Z"},{"id":"UBUNTU-CVE-2026-46338","description":"PyMdown Extensions is a set of extensions for the Python-Markdown markdown project. From 10.0.1 until 10.21.3, pymdownx.snippets uses a string-prefix containment check in SnippetPreprocessor.get_snippet_path() in pymdownx/snippets.py when `restrict_base_path: True`, allowing markdown snippet directives to read files from sibling paths that share the same base_path prefix, such as docs and docs_internal. This is a regression of CVE-2023-32309. This issue is fixed in version 10.21.3.","status":"published","published":"2026-07-16T19:16:00Z","modified":"2026-10-17T12:55:37.580629718Z","cvss":[{"version":"3.1","score":4.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T16:56:30.797112Z"},{"id":"ROOT-APP-PYPI-CVE-2026-46338","title":"CVE-2026-46338 in pymdown-extensions - Patched by Root","description":"Root has patched CVE-2026-46338 in the pymdown-extensions package for Root:PyPI. Multiple fixed versions available.","status":"published","published":"2026-09-14T12:37:52Z","modified":"2026-09-14T13:15:05.339977851Z","updated_at":"2026-11-02T15:53:28.259095Z"}]}}
