Skip to content

Commit 92cc649

Browse files
chore(deps): update dependency pymdown-extensions to v10.21.3 [security] (#34)
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [pymdown-extensions](https://redirect.github.com/facelessuser/pymdown-extensions) | `10.21` → `10.21.3` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/pymdown-extensions/10.21.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pymdown-extensions/10.21/10.21.3?slim=true) | --- ### Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path [CVE-2026-46338](https://nvd.nist.gov/vuln/detail/CVE-2026-46338) / [GHSA-62q4-447f-wv8h](https://redirect.github.com/advisories/GHSA-62q4-447f-wv8h) <details> <summary>More information</summary> #### Details ##### 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 #&#8203;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, #&#8203;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/ -q` → **738 passed** (737 baseline + 1 new regression test). No regressions. ##### Affected versions `>= 10.0.1, <= 10.21.2` #### Severity - CVSS Score: 4.3 / 10 (Medium) - Vector String: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N` #### References - [https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-62q4-447f-wv8h](https://redirect.github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-62q4-447f-wv8h) - [https://github.com/facelessuser/pymdown-extensions/pull/2039](https://redirect.github.com/facelessuser/pymdown-extensions/pull/2039) - [https://github.com/advisories/GHSA-62q4-447f-wv8h](https://redirect.github.com/advisories/GHSA-62q4-447f-wv8h) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-62q4-447f-wv8h) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Release Notes <details> <summary>facelessuser/pymdown-extensions (pymdown-extensions)</summary> ### [`v10.21.3`](https://redirect.github.com/facelessuser/pymdown-extensions/releases/tag/10.21.3) [Compare Source](https://redirect.github.com/facelessuser/pymdown-extensions/compare/10.21.2...10.21.3) #### 10.21.3 - **FIX**: Fix regression that allows a snippet to be loaded outside of the base path using directory traversal when `restrict_base_path` is enabled (the default). Found by [@&#8203;gistrec](https://redirect.github.com/gistrec). ### [`v10.21.2`](https://redirect.github.com/facelessuser/pymdown-extensions/releases/tag/10.21.2): 10.21. 2 [Compare Source](https://redirect.github.com/facelessuser/pymdown-extensions/compare/10.21...10.21.2) #### 10.21.2 - **FIX**: Highlight: Latest Pygments versions cannot handle a "filename" for code block titles of `None`. </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE4MC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=--> Co-authored-by: renovate-rosenstein-app[bot] <276121409+renovate-rosenstein-app[bot]@users.noreply.github.com>
1 parent 74ec630 commit 92cc649

1 file changed

Lines changed: 3 additions & 3 deletions

File tree

docs/uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)