CVE-2026-67422: facelessuser pymdown-extensions · ReDoS
Affects facelessuser pymdown-extensionsSource CVE Database V5
Mentioned vulnerabilities
Not known to be exploited · CVSS 7.5 High : upgrade to 11.0.1 soon.
Data as of 5 Nov 2026, 00:01 UTC
- EPSS
- 0.6% chance of exploitation in 30 days – higher than 49% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
- Patch
- Available – fixed in 11.0.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HExploitation evidence
Disclosed: evidence found.
CVE published 6 Aug 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 5 Nov 2026.
What to do
Upgrade facelessuser pymdown-extensions to 11.0.1.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
| facelessuserpymdown-extensions | Affected
| Fixed in
|
PyPIpymdown-extensionspkg:pypi/pymdown-extensionsOther threats | Affected
| Fixed in– |
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
AI-suggested
Upgrade pymdown-extensions to version 11.0.1 or later, where the inefficient regular expressions causing catastrophic backtracking have been fixed. Until upgraded, avoid rendering untrusted Markdown using the vulnerable inline processors or disable the affected extensions if possible. Patch status is confirmed by the vendor fix 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'(?<!\^)(\^)(?![\^\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.