Sep 30, 2026·6 min read·6 visits
A regular expression flaw in league/commonmark allows attackers to bypass the DisallowedRawHtml filter using unclosed tags at block boundaries, resulting in Stored Cross-Site Scripting (XSS).
A security vulnerability in league/commonmark versions 1.3.0 through 2.10.1 allows remote attackers to bypass Stored Cross-Site Scripting (XSS) protections in the DisallowedRawHtml extension. Due to an validation logic flaw in the regular expression parser, specifically handling bare, unclosed HTML blocks ending at the string boundary, raw HTML tags can be passed to the rendered output. When combined with browser-side parsing heuristics, an attacker can execute arbitrary JavaScript in the context of the user session.
The PHP Markdown parser library league/commonmark is widely used to convert Markdown text into compliant HTML. To secure applications from malicious raw HTML insertion, the library provides the DisallowedRawHtml extension (which is enabled automatically under GitHub Flavored Markdown config). This extension is designed to sanitize and escape hazardous HTML tags, such as <script>, <iframe>, or <style>, preventing potential script execution in a client browser.
However, a severe design flaw exists within the input validation structure of the DisallowedRawHtml extension. The module employs a regular expression pattern to intercept and escape dangerous tags. When processing highly malformed or truncated HTML blocks, the parser can fail to match and escape these elements, leaving raw script tags intact within the rendered HTML payload.
This flaw represents a Stored Cross-Site Scripting (XSS) vulnerability. Since the processed Markdown is stored in the database (e.g., comments, forum content, wiki pages) and rendered to other users, attackers can execute arbitrary JavaScript in the sessions of unsuspecting users who view the generated page.
The root cause of this vulnerability lies in an impedance mismatch between the CommonMark HTML block parser and the DisallowedRawHtmlRenderer validation regex. According to the CommonMark and GFM specifications, a line starting with an HTML tag name (such as <script or <iframe) with no trailing brackets or whitespace is interpreted as the beginning of a valid HTML block.
When league/commonmark compiles this into an HtmlBlock node, it retains the exact text segment. This output text ends abruptly at the tag name boundary itself (e.g., <script). It does not append a closing bracket or space. Prior to the fix, the DisallowedRawHtmlRenderer executed the following regular expression to intercept the disallowed tag names:
$regex = \sprintf('/<(\/?(?:%s)[\s\/>])/i', \implode('|', \array_map('preg_quote', $tags)));The trailing character class [\s\/>] requires that the matched tag name be immediately followed by either a space, a forward slash, or a closing angle bracket. Because the compiled HtmlBlock ends exactly with the last character of the tag name (the letter 't' in <script), there is no trailing character available. The regex match fails entirely, and the unescaped tag is passed directly into the final document.
When a browser parses the resulting HTML document, it encounters the raw <script string. To ensure high compatibility with poorly structured web pages, modern web browsers automatically attempt to repair unclosed tags. The browser treats subsequent HTML attributes on the next lines as the attributes of the open <script tag, leading to script execution.
The vulnerability was corrected in commit 411afcc2a7402756d96c89af8882c724d12d47ca by extending the regular expression to support end-of-string boundaries. Below is a comparison of the vulnerable and patched regex implementations in DisallowedRawHtmlRenderer.php.
- $regex = \sprintf('/<(\/?(?:%s)[\s\/>])/i', \implode('|', \array_map('preg_quote', $tags)));
+ // The tag name may also end the rendered HTML: the block parser accepts a bare
+ // `<script` line, and browsers treat the following block as its attributes.
+ $regex = \sprintf('/<(\/?(?:%s)(?:[\s\/>]|$))/i', \implode('|', \array_map('preg_quote', $tags)));In the vulnerable implementation, the regex required an explicit trailing delimiter character. In the patched code, a non-capturing group (?:[\s\/>]|$) was introduced. This alternation ensures that the pattern matches the tag name if it is followed by a standard delimiter OR if it is situated at the absolute end of the target string ($).
This correction is robust and complete. It prevents the exploitation of any block structure where the tag terminates the string, regardless of whether the tag is at the end of the entire document or nested within structural blocks. Variant attacks trying to bypass the check by abusing boundary conditions are neutralized because the regex engine now treats the string boundary as a valid terminator.
To exploit this vulnerability, an attacker must have permission to submit Markdown text that is subsequently rendered to other users. The configuration parameter html_input must be set to allow (the library default), and the DisallowedRawHtml extension must be active (which is typical when using GFM).
An attacker can construct a payload where the disallowed tag is isolated on its own line without any trailing characters, thereby triggering the HTML block parsing rules. The attacker then places the execution context or external resource link on a subsequent block, which the browser's loose parsing engine will merge.
An example payload utilizes a <script tag followed by a blank line and an element that looks like an attribute list:
<div>
<script
<span src="/evil.js">This input translates to the following rendered HTML output:
<div>
<script
<span src="/evil.js">When a browser reads this, it parses the open <script tag, ignores the intermediate whitespace, and interprets the src="/evil.js" attribute as belonging directly to the script element, downloading and executing the remote script.
The impact of this vulnerability is Stored Cross-Site Scripting (XSS). If successfully exploited, the attacker can execute arbitrary client-side JavaScript within the browser context of any user who views the page containing the malicious rendered Markdown.
This execution capability enables several critical attack paths. Attackers can steal session identifiers, session cookies (if not marked with the HttpOnly flag), or access tokens stored in LocalStorage. They can also perform unauthorized actions on behalf of the victim, alter the DOM to phish for user credentials, or deploy client-side keyloggers to capture sensitive user inputs.
The vulnerability is classified with a CVSS v3.1 score of 6.1 (Medium). The vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N highlights that while exploitation requires zero privileges (PR:N) and is of low complexity (AC:L), it does require a victim user to navigate to and view the page containing the injected content (UI:R), and triggers a scope change (S:C) because execution happens in the browser context.
The primary and recommended mitigation is upgrading the league/commonmark package to version 2.10.2 or later. This release updates the core regex logic to handle string boundaries gracefully, ensuring that bare tag names are properly matched and escaped.
To update the dependency in a PHP application utilizing Composer, execute the following command:
composer update league/commonmarkIf upgrading the library is not immediately possible due to dependency constraints, you can temporarily mitigate the vulnerability by adjusting the library configuration. Setting the html_input option to escape or strip prevents the parser from rendering any raw HTML tags, neutralizing the bypass vector.
use League\CommonMark\MarkdownConverter;
$config = [
'html_input' => 'escape', // Escapes all user HTML, neutralizing XSS bypasses
];
$converter = new MarkdownConverter($config);CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
league/commonmark thephpleague | >= 1.3.0, <= 2.10.1 | 2.10.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (AV:N) |
| CVSS | 6.1 (Medium) |
| EPSS Score | N/A (No CVE assigned) |
| Exploit Status | poc |
| KEV Status | false |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
An uncontrolled resource consumption vulnerability in FasterXML jackson-databind allows remote unauthenticated attackers to cause a Denial of Service (DoS) via crafted JSON payloads containing out-of-order forward references in identity-enabled collections or maps.
A DOM-based Cross-Site Scripting (DOM XSS) vulnerability in DOMPurify allows attackers to bypass sanitization when using the in-place sanitization configuration (IN_PLACE: true) combined with custom hooks. If custom hooks detach elements during post-processing phases, nested malicious payloads can escape sterilization and execute in the browser's context.
A security vulnerability in serialize-javascript v7.1.1 allows Cross-Site Scripting (XSS) due to an overly greedy regular expression (SCRIPT_CLOSE_REGEXP) used during function serialization. Two secondary defects involving a spoofed toString() validation bypass and a stateful native code validator are also addressed in the fixed version v7.1.2.
A Denial of Service (DoS) vulnerability exists in the PyJWT library when parsing unverified token payloads containing deeply nested JSON structures. Because PyJWT fails to catch RecursionError during payload parsing, an unauthenticated remote attacker can crash the application thread or worker by sending a specially crafted token.
An improper output encoding and escaping vulnerability (CWE-116) in Vercel Satori allows unauthenticated remote attackers to perform markup injection in dynamic Open Graph images generated via Next.js's ImageResponse. Unsanitized parameter interpolation into SVG elements breaks XML structural boundaries. This exposes downstream parsing, rasterization, and rendering pipelines to Server-Side Request Forgery (SSRF), Local File Read, and Remote Code Execution (RCE).
An uncontrolled recursion vulnerability exists in PyJWT from version 2.13.0 to 2.14.0. The vulnerability allows remote, unauthenticated attackers to cause a Denial of Service (DoS) via crafted JWT headers that trigger stack exhaustion during JSON decoding.