Skip to content

Update dependency pymdown-extensions to v10.21.3 [SECURITY]#33

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-pymdown-extensions-vulnerability
Open

Update dependency pymdown-extensions to v10.21.3 [SECURITY]#33
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-pymdown-extensions-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate Bot commented Apr 15, 2026

This PR contains the following updates:

Package Change Age Confidence
pymdown-extensions 10.14.310.21.3 age confidence

PyMdown Extensions has a ReDOS bug in its Figure Capture extension

CVE-2025-68142 / GHSA-r6h4-mm7h-8pmq

More information

Details

Impact

This issue describes a ReDOS bug found within the figure caption extension (pymdownx.blocks.caption ).

In systems that take unchecked user content, this could cause long hangs when processing the data if a malicious payload was crafted.

Patches

This issue is patched in Release 10.16.1.

Workarounds

Some possible workarounds

If users are concerned about this vulnerability and process unknown user content without timeouts or other safeguards in place to prevent really large, malicious content being aimed at systems, the use of pymdownx.blocks.caption could be avoided until the library is updated to 10.16.1+.

References

The original issue https://github.com/facelessuser/pymdown-extensions/issues/2716.

Description

The original issue came through PyMdown Extensions' normal issue tracker instead of the typical security flow: https://github.com/facelessuser/pymdown-extensions/issues/2716. Because this came through the normal issue flow, it was handled as a normal issue. In the future, PyMdown Extensions will ensure such issues, even if prematurely made public through the normal issue flow, are redirected through the typical security process.

The regular expression pattern in question is as follows:

RE_FIG_NUM = re.compile(r'^(\^)?([1-9][0-9]*(?:.[1-9][0-9]*)*)(?= |$)')

The POC was provided by @​ShangzhiXu

import re
import time

regex_pattern = re.compile(r'^(\^)?([1-9][0-9]*(?:.[1-9][0-9]*)*)(?= |$)')

for i in range(50, 500, 50):
    long_string = '1' * i + 'a'
    start_time = time.time()
    match = re.match(regex_pattern, long_string)
    end_time = time.time()
    print(f"long_string execution time: {end_time - start_time:.6f} s")

The issue with the above pattern is that . was used, which accepts any character when we meant to use \.. The fix was to update the pattern to:

RE_FIG_NUM = re.compile(r'^(\^)?([1-9][0-9]*(?:\.[1-9][0-9]*)*)(?= |$)')

Relevant PR with fix: https://github.com/facelessuser/pymdown-extensions/pull/2717

Version(s) & System Info
  • Operating System: Any
  • Python Version: Any

Severity

  • CVSS Score: 2.7 / 10 (Low)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:U

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path

CVE-2026-46338 / GHSA-62q4-447f-wv8h

More information

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 #​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:

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:

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:

-                        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

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

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

facelessuser/pymdown-extensions (pymdown-extensions)

v10.21.3

Compare Source

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 @​gistrec.

v10.21.2: 10.21. 2

Compare Source

10.21.2

  • FIX: Highlight: Latest Pygments versions cannot handle a "filename" for code block titles of None.

v10.21

10.21

  • NEW: Caption: Add support for specifying not only IDs but classes and arbitrary attributes. Initial work by
    @​joapuiib.
  • FIX: MagicLink: Fix a matching pattern for Bitbucket repo.

v10.20

Compare Source

10.20

  • NEW: Quotes: New blockquotes extension added that uses a more modern approach when compared to Python Markdown's
    default. Quotes specifically will not group consecutive blockquotes together in the same lazy fashion that the
    default Python Markdown does which follows a more modern trend to how parsers these days handle block quotes.

    In addition, Quotes also provides an optional feature to enable specifying callouts/alerts in the style used by
    GitHub and Obsidian.

v10.19.1

Compare Source

10.19.1

  • FIX: Arithmatex: Fix issue where block $$ math used inline within a paragraph could result in nested math
    parsing.

v10.19

Compare Source

10.19

  • NEW: Emoji: Update Twemoji to use Unicode 16.
  • NEW: Critic: Roll back view mode deprecation as some still like to use it, though further enhancements to this
    mode are not planned.

v10.18

Compare Source

10.18

  • NEW: Critic: view mode has been deprecated. To avoid warnings or future issues, explicitly set mode to
    either accept or reject. In the future, the new default will be accept and the view mode will be removed
    entirely.
  • FIX: Block Admonition: important should have always been available as a default.

v10.17.2

Compare Source

10.17.2

  • FIX: Blocks: Blocks extensions will now better handle nesting of indented style Admonitions, Details, and Tabbed
    and other non-conflicting blocks.

v10.17.1

Compare Source

10.17.1

  • FIX: Fix an issue where Highlight can override another extension in the "registered" list in Python Markdown.

v10.17

Compare Source

10.17

  • NEW: Allow specifying static IDs in caption block headers via #id syntax.

v10.16.1: 10.6.1

Compare Source

10.16.1

  • FIX: Inefficient regular expression pattern for figure caption numbers.

v10.16

Compare Source

10.16

  • NEW: Add early support for Python 3.14.
  • NEW: Drop support for Python 3.8.
  • NEW: Snippets: Added max_retries and backoff_retries options to configure new retry logic for HTTP 429
    errors (Too Many Requests client error).
  • NEW: Caption: Prefix templates are now preserved exactly as specified allowing the insertion of HTML tags if
    desired.
  • FIX: Caption: Fix issue where manual numbers in auto were not respected appropriately.

v10.15

Compare Source

10.15.0

  • NEW: SuperFences: Add relaxed_headers option which can tolerate bad content in the fenced code header. When
    enabled, code blocks with bad content in the header will likely still convert into code blocks, often respecting
    the specified language.
  • NEW: Add type hints to the Blocks interface and a few additional files.
  • FIX: Blocks: Fix some corner cases of nested blocks with lists.
  • FIX: Tab and Tabbed: Fix a case where tabs could fail if combine_header_slug was enabled and there was no
    header.

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • ""
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

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.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the dependencies Pull requests that update a dependency file label Apr 15, 2026
@renovate renovate Bot changed the title Update dependency pymdown-extensions to v10.16.1 [SECURITY] Update dependency pymdown-extensions to v10.16.1 [SECURITY] - autoclosed Apr 27, 2026
@renovate renovate Bot closed this Apr 27, 2026
@renovate renovate Bot deleted the renovate/pypi-pymdown-extensions-vulnerability branch April 27, 2026 17:41
@renovate renovate Bot changed the title Update dependency pymdown-extensions to v10.16.1 [SECURITY] - autoclosed Update dependency pymdown-extensions to v10.16.1 [SECURITY] Apr 27, 2026
@renovate renovate Bot reopened this Apr 27, 2026
@renovate renovate Bot force-pushed the renovate/pypi-pymdown-extensions-vulnerability branch 2 times, most recently from 71452f8 to a80b094 Compare April 27, 2026 21:43
@renovate renovate Bot force-pushed the renovate/pypi-pymdown-extensions-vulnerability branch from a80b094 to eb20bfb Compare May 18, 2026 12:36
@renovate renovate Bot changed the title Update dependency pymdown-extensions to v10.16.1 [SECURITY] Update dependency pymdown-extensions to v10.21.3 [SECURITY] May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants