The setting you were told to switch on before rendering other people's Markdown, allow_unsafe_links => false, does not stop this one. A flaw disclosed on August 3 in league/commonmark, the most widely used Markdown parser for PHP, lets an attacker hide a javascript: link inside content the library is supposed to sanitize. When a browser later renders that content, the script runs. The maintainers assigned it CVE-2026-71478 and shipped a fix in version 2.9.0.
The reason it matters is not the severity number. At CVSS 6.1 this is a moderate bug. It matters because it is the second time the same defense in this library has been walked around, and because it defeats the exact hardening the project tells operators to turn on for untrusted input.
Who is exposed, and who is not
Not every commonmark install. The bug lives in the Attributes extension, an opt-in feature that lets authors attach HTML attributes to elements with a {#id .class} syntax. If you run commonmark the common way, through Laravel's Markdown helper or the default converters, the Attributes extension is off and this path does not apply to you.
You are exposed when two things are both true: you enable the Attributes extension, and you render Markdown that untrusted people can write. Comments, profile fields, support tickets, wiki pages, anything user-submitted. Affected versions run from 1.5.0 through 2.8.3. The fix is 2.9.0.
The same filter, slipped past twice
commonmark blocks dangerous link schemes with an anchored prefix pattern: it inspects the start of an href or src value and rejects anything that begins with javascript: and its cousins. The problem is that a browser does not read a link the way a plain string check does. Before a browser parses a URL scheme, it removes tab, carriage-return and line-feed bytes from anywhere in the value, and it drops leading non-printable control bytes. So a link that carries an invisible tab inside the word javascript, or a stray control byte in front of it, looks harmless to the filter and reads as live script to the browser. The library checked the raw string; the browser checked the cleaned one. That gap is the whole bug. It was reported by researcher Tung NGo.
What makes it worth writing down is the repeat. An earlier flaw, CVE-2025-46734, was fixed in 2.7.0 by making the Attributes extension respect allow_unsafe_links on href and src. That closed the front door. It did not normalize control bytes before the check, so a side window stayed open. CVE-2026-71478 is that side window.
| Advisory | The gap | What the fix changed | Fixed in |
|---|---|---|---|
| CVE-2025-46734 | The Attributes extension ignored allow_unsafe_links on href and src | Made href and src honor allow_unsafe_links | 2.7.0 |
| CVE-2026-71478 | Control bytes hidden in a scheme slipped past that filter, even with allow_unsafe_links off | Normalizes control bytes before the unsafe-link check | 2.9.0 |
This is the recurring failure mode of denylist filtering: you try to enumerate every dangerous input, against a browser that keeps its own rules for what a string means. The durable fix runs the other direction. Normalize the value first, the same way the browser will, then validate. Or drop the denylist and allow only a known set of safe schemes, such as http, https and mailto. Version 2.9.0 adds the normalization step the 2.7.0 fix skipped. It closes this instance. Whether it closes the class is a question the next researcher will answer.
How you would know, not just how you patch
Patching removes the hole. It does not tell you whether someone already planted a payload in content you stored while the hole was open. Because this is a stored or reflected script issue, the evidence lives in your data and your rendered pages, not only in your dependency file.
Two things to do. First, search stored user content and rendered output for link values whose scheme contains a tab, carriage-return, line-feed or leading control byte, or that decode to a javascript: destination. Those are the fingerprints of this technique and they do not appear in legitimate links. Second, put a Content Security Policy in front of the problem. A policy that forbids inline script execution turns a successful injection into a blocked one, and it guards you against the next filter bypass as well as this one.
At fleet scale the first question is simpler: which of your hosts even run an affected version. Continuous vulnerability detection answers that by inventorying installed packages and flagging the ones that match a known CVE, so you patch the boxes that are exposed instead of guessing. Pair it with baseline hardening so the Content Security Policy is already in place before you need it.
Update to 2.9.0, then look at what you feed the Attributes extension
Move commonmark to 2.9.0. That is the fix, and it is not optional if you render untrusted Markdown with the Attributes extension enabled. Then make the second decision the CVE should prompt: do you really need attribute syntax on content that strangers can write? For many applications the Attributes extension is a convenience for trusted authors, and leaving it on for public input buys risk for little return. Turn it off where the input is untrusted, keep it where the authors are your own team, and let the Content Security Policy catch what slips.