Skip to main content
Vulnerability Database/CVE-2026-86431

CVE-2026-86431: Thephpleague Commonmark XSS Vulnerability

CVE-2026-86431 is a cross-site scripting flaw in Thephpleague Commonmark that allows attackers to bypass attribute filters using form feed bytes. This post explains technical details, affected versions, impact, and mitigation.

Published:

CVE-2026-86431 Overview

CVE-2026-86431 is a cross-site scripting [CWE-79] vulnerability in the league/commonmark (thephpleague/commonmark) PHP Markdown parser. Versions >= 2.7.0 and < 2.9.1 are affected when the AttributesExtension is enabled. An attacker can prefix an attribute name with a single U+000C form feed byte to bypass the AttributesHelper::filterAttributes()on* event-handler filter. The same prefix also defeats the allow_unsafe_links check, allowing javascript: URIs through href and src attributes. Exploitation requires processing untrusted Markdown; the injected script executes when the rendered HTML is viewed. The issue is fixed in version 2.9.1.

Critical Impact

Attackers can inject arbitrary JavaScript event handlers and javascript: URIs into rendered HTML output, leading to session hijacking, credential theft, and account takeover in applications that render user-supplied Markdown.

Affected Products

  • thephpleague/commonmark >= 2.7.0
  • thephpleague/commonmark < 2.9.1
  • Any PHP application using AttributesExtension to process untrusted Markdown

Discovery Timeline

  • 2026-09-07 - CVE-2026-86431 published to NVD
  • 2026-09-09 - Last updated in NVD database

Technical Details for CVE-2026-86431

Vulnerability Analysis

The AttributesExtension in league/commonmark allows Markdown authors to attach HTML attributes to elements using syntax such as {#id .class attr=value}. To prevent script injection, AttributesHelper::filterAttributes() rejects any attribute whose name begins with on (for example, onclick, onerror, onload). The filter normalizes attribute names using PHP's trim() before performing the comparison.

PHP's default trim() strips whitespace characters including space, tab, newline, carriage return, null byte, and vertical tab, but it does not strip U+000C (form feed, \\x0C). Browsers, however, treat form feed as HTML whitespace when parsing tag attributes. An attribute name such as \\x0Conclick therefore passes the filter as a non-on* name yet is parsed by browsers as a genuine onclick event handler.

The same parsing asymmetry defeats the allow_unsafe_links check on href and src attributes. Prefixing javascript: with a form feed byte causes the URI check to miss the dangerous scheme while browsers still execute it. This allows attackers to inject executable JavaScript even when allow_unsafe_links is set to false.

Root Cause

The root cause is a mismatch between PHP string normalization and HTML attribute parsing. Server-side filtering uses trim(), which omits U+000C from its default character set, while browsers treat U+000C as valid whitespace in attribute name and URI contexts. The result is an inconsistent trust boundary between the sanitizer and the consumer.

Attack Vector

An attacker submits Markdown content containing an attribute block with a form feed prefix, for example [link](/){\\x0Conclick="alert(1)"} or a link with a form-feed-prefixed javascript: URI. When the application renders the Markdown to HTML and a victim views the page, the injected handler executes in the victim's browser context.

php
// Patch: src/Extension/Attributes/AttributesExtension.php
 use League\CommonMark\Extension\Attributes\Event\AttributesListener;
 use League\CommonMark\Extension\Attributes\Parser\AttributesBlockStartParser;
 use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
-use League\CommonMark\Extension\ExtensionInterface;
+use League\CommonMark\Extension\ConfigurableExtensionInterface;
+use League\Config\ConfigurationBuilderInterface;
+use Nette\Schema\Expect;

-final class AttributesExtension implements ExtensionInterface
+final class AttributesExtension implements ConfigurableExtensionInterface
 {
+    public function configureSchema(ConfigurationBuilderInterface $builder): void
+    {
+        $builder->addSchema('attributes', Expect::structure([
+            'allow' => Expect::arrayOf('string')->default([]),
+        ]));
+    }
+
     public function register(EnvironmentBuilderInterface $environment): void
     {
+        $allowList        = $environment->getConfiguration()->get('attributes.allow');
+        $allowUnsafeLinks = $environment->getConfiguration()->get('allow_unsafe_links');
+
         $environment->addBlockStartParser(new AttributesBlockStartParser());
         $environment->addInlineParser(new AttributesInlineParser());
-        $environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
+        $environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener($allowList, $allowUnsafeLinks), 'processDocument']);
     }
 }

Source: thephpleague/commonmark commit 43207253. The patch replaces the implicit deny-list with an explicit allow-list, propagates the allow_unsafe_links setting into the listener, and refactors AttributesExtension to implement ConfigurableExtensionInterface.

Detection Methods for CVE-2026-86431

Indicators of Compromise

  • Markdown submissions containing U+000C (\\x0C, form feed) bytes inside { ... } attribute blocks.
  • Stored or reflected HTML output containing whitespace-prefixed on* attribute names such as \\x0Conclick, \\x0Conerror, or \\x0Conload.
  • Rendered anchor tags where href or src values begin with a non-printable byte followed by javascript:.
  • Composer lockfiles (composer.lock) pinning league/commonmark between 2.7.0 and 2.9.0 with AttributesExtension registered in the environment configuration.

Detection Strategies

  • Inventory PHP applications with composer show league/commonmark and flag any version below 2.9.1.
  • Grep application code for AttributesExtension and CommonMarkConverter initialization to identify exposed rendering paths.
  • Add a WAF or input filter rule that rejects request bodies containing form feed bytes inside Markdown fields.
  • Scan stored Markdown content and rendered HTML caches for form feed characters adjacent to attribute-like tokens.

Monitoring Recommendations

  • Log all Markdown rendering requests with source user identity to enable post-exploitation attribution.
  • Deploy a Content Security Policy (CSP) that blocks inline event handlers and javascript: URIs to surface XSS attempts as violation reports.
  • Correlate CSP violation reports with rendering endpoints to identify targeted user-generated content.
  • Alert on outbound requests from user browsers to unexpected domains shortly after viewing pages that render Markdown.

How to Mitigate CVE-2026-86431

Immediate Actions Required

  • Upgrade league/commonmark to version 2.9.1 or later using composer require league/commonmark:^2.9.1.
  • Audit all rendering pipelines that enable AttributesExtension and disable the extension where it is not required.
  • Rotate session tokens for users who may have viewed attacker-controlled Markdown during the exposure window.
  • Enforce a strict Content Security Policy on any page that renders user-supplied Markdown.

Patch Information

The fix landed in commit 43207253ea5f14867c77c697cd3838c446cadcea and is released in league/commonmark version 2.9.1. The patched AttributesListener accepts an explicit allow-list and honors the allow_unsafe_links configuration, replacing the previous deny-list approach that was bypassable via form feed prefixing. See the GitHub Security Advisory GHSA-f8fg-pg57-v4j8 and the VulnCheck advisory for additional context.

Workarounds

  • Remove AttributesExtension from the environment configuration until patching is complete.
  • Pre-process untrusted Markdown to strip U+000C bytes before passing input to the converter.
  • Post-process rendered HTML with a strict sanitizer such as HTML Purifier configured to remove all on* attributes and non-http(s) URI schemes.
  • Serve rendered content with a Content Security Policy that disallows inline scripts and javascript: URIs.
bash
# Upgrade to the patched release
composer require league/commonmark:^2.9.1
composer update league/commonmark

# Verify installed version
composer show league/commonmark | grep versions

# Optional: remove form feed bytes from Markdown input before rendering
# php -r 'echo str_replace("\\x0C", "", $input);'

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.