One emoji, quadratic time: the Markdown bug that DoSes your site without a payload
Here's a vulnerability that doesn't crash anything, doesn't leak anything, and doesn't let anyone run code — and it can still take your site down with a single comment. It's a denial-of-service in league/commonmark, the Markdown parser sitting quietly behind an enormous slice of the PHP world, and the trigger is one non-ASCII character. It's the kind of bug nobody threat-models, which is exactly why it's worth writing about.
What it is
CVE-2026-71488 (advisory GHSA-2q4p-g7hv-5rgv) is a quadratic-complexity blowup fixed in league/commonmark 2.9.0 on 3 August 2026. Everything from 0.6.0 up to 2.9.0 is affected. The CVSS is 7.5 — availability-only, no confidentiality or integrity impact — which reads as "just a DoS" and tends to get deprioritized. I'd argue that's a mistake, not because the score is wrong, but because of where this library lives.
The mechanism is genuinely elegant
The parser tracks positions in the text by character, but its regular-expression matches report positions by byte. For plain ASCII those are the same number, so nothing bad happens. But a UTF-8 character can be several bytes, so the moment one appears on a line, the two counting systems diverge — and several parsing paths then repeatedly rescan growing chunks of the line to translate between byte and character positions. That rescanning is what turns linear work into O(n²). As the advisory puts it, doubling the length of an affected line makes the parser do roughly four times the work. The autolink extension (part of GitHub-flavored Markdown, which lots of sites enable) amplifies it further by re-validating the rest of the line at every URL-like prefix.
So an attacker doesn't need a payload. They need a long line with an emoji in it. A few kilobytes of the right input, and one request pins a CPU core for seconds.
The bug only appears with non-ASCII input. Which means an all-ASCII test corpus — the kind almost everyone writes — would never surface it. The vulnerability was hiding in the exact place nobody points their tests.
Why the blast radius is large
This isn't a niche package. Laravel's Str::markdown() and its Markdown mailables run on league/commonmark (Laravel switched to it years ago). Drupal's CommonMark/Markdown contrib stack uses it. Statamic, static-site generators, and a long tail of CMSes build on it — it's one of the most-installed PHP packages there is. Anywhere a site turns user-submitted Markdown into HTML — a comment box, a CKEditor field, a profile bio, a support ticket — is a place this input can arrive. The fix is trivial (upgrade to 2.9.0). The interesting part is the lesson.
The class of bug nobody tests for
We threat-model for the dramatic stuff: RCE, SQL injection, auth bypass. Algorithmic-complexity DoS — ReDoS and its cousins — barely registers, because it doesn't fit the mental model of "attacker gets something." Here the attacker gets nothing except your CPU, and that turns out to be plenty. The defenses we lean on don't catch it, either: crash-fuzzing looks for segfaults and exceptions, not for "200 bytes of input that costs 4× the CPU when you double it." A parser can be perfectly memory-safe and still be a DoS waiting to happen.
The honest takeaway I'm keeping: when you depend on a parser that eats untrusted input — Markdown, a template language, a regex over user data, a date parser — benchmark it against adversarial input, not average input. Feed it pathological worst-cases and watch how the time scales with the size. If doubling the input more than doubles the time, that's not a performance note. That's a security finding. And test with multibyte input, because as this bug shows, the whole failure can live in the space between a byte and a character.
What to do
Upgrade league/commonmark to 2.9.0 or later — a composer update league/commonmark and you're done; the release also bundled a handful of other DoS fixes and an XSS fix, so it's worth doing regardless. If you can't upgrade immediately, cap the size of Markdown input you'll accept and put a timeout around the render. And run composer audit while you're in there — this is exactly the kind of advisory it'll flag, and exactly the kind that's easy to miss because it never announced itself with a crash.