--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:php-league-commonmark
User: [email protected]
Usertags: pu
Hi,
As agreed with the security team, I’d like to address the following
three security issues via the next point release.
* Fix XSS in AttributesExtension [CVE-2025-46734]
* Fix DisallowedRawHtml bypass via newline/tab in tag names [CVE-2026-30838]
* Fix DomainFilteringAdapter hostname boundary bypass [CVE-2026-33347]
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
Regards,
taffit
diff -Nru php-league-commonmark-2.3.9/debian/changelog php-league-commonmark-2.3.9/debian/changelog
--- php-league-commonmark-2.3.9/debian/changelog 2023-02-15 18:38:28.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/changelog 2026-03-26 08:15:50.000000000 +0100
@@ -1,3 +1,12 @@
+php-league-commonmark (2.3.9-1+deb12u1) bookworm; urgency=medium
+
+ * Track Bookworm branch
+ * Fix XSS in AttributesExtension [CVE-2025-46734]
+ * Fix DisallowedRawHtml bypass via newline/tab in tag names [CVE-2026-30838]
+ * Fix DomainFilteringAdapter hostname boundary bypass [CVE-2026-33347]
+
+ -- David Prévot <[email protected]> Thu, 26 Mar 2026 08:15:50 +0100
+
php-league-commonmark (2.3.9-1) unstable; urgency=medium
[ Colin O'Dell ]
diff -Nru php-league-commonmark-2.3.9/debian/control php-league-commonmark-2.3.9/debian/control
--- php-league-commonmark-2.3.9/debian/control 2023-02-15 18:36:22.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/control 2026-03-26 08:15:50.000000000 +0100
@@ -18,7 +18,7 @@
pkg-php-tools (>= 1.41~)
Standards-Version: 4.6.2
Homepage: https://commonmark.thephpleague.com/
-Vcs-Git: https://salsa.debian.org/php-team/pear/php-league-commonmark.git -b debian/latest
+Vcs-Git: https://salsa.debian.org/php-team/pear/php-league-commonmark.git -b debian/bookworm-security
Vcs-Browser: https://salsa.debian.org/php-team/pear/php-league-commonmark
Rules-Requires-Root: no
diff -Nru php-league-commonmark-2.3.9/debian/gbp.conf php-league-commonmark-2.3.9/debian/gbp.conf
--- php-league-commonmark-2.3.9/debian/gbp.conf 2022-01-25 19:18:49.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/gbp.conf 2026-03-26 08:15:50.000000000 +0100
@@ -1,5 +1,5 @@
[DEFAULT]
-debian-branch = debian/latest
+debian-branch = debian/bookworm-security
filter = [ '.gitattributes' ]
pristine-tar = True
upstream-vcs-tag = %(version%~%-)s
diff -Nru php-league-commonmark-2.3.9/debian/patches/0002-Fix-XSS-in-AttributesExtension.patch php-league-commonmark-2.3.9/debian/patches/0002-Fix-XSS-in-AttributesExtension.patch
--- php-league-commonmark-2.3.9/debian/patches/0002-Fix-XSS-in-AttributesExtension.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/patches/0002-Fix-XSS-in-AttributesExtension.patch 2026-03-26 08:15:50.000000000 +0100
@@ -0,0 +1,296 @@
+From: Colin O'Dell <[email protected]>
+Date: Mon, 5 May 2025 07:53:56 -0400
+Subject: Fix XSS in AttributesExtension
+
+Origin: backport, https://github.com/thephpleague/commonmark/commit/43207253ea5f14867c77c697cd3838c446cadcea
+Bug: https://github.com/thephpleague/commonmark/security/advisories/GHSA-3527-qv2q-pfvx
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2025-46734
+---
+ src/Extension/Attributes/AttributesExtension.php | 18 +++++-
+ .../Attributes/Event/AttributesListener.php | 15 ++++-
+ src/Extension/Attributes/Util/AttributesHelper.php | 38 +++++++++++++
+ .../Extension/Attributes/data/allowlist.html | 3 +
+ .../Extension/Attributes/data/allowlist.md | 12 ++++
+ .../Attributes/data/js_event_attributes.html | 1 +
+ .../Attributes/data/js_event_attributes.md | 1 +
+ .../Attributes/data/unsafe_links_allowed.html | 1 +
+ .../Attributes/data/unsafe_links_allowed.md | 5 ++
+ .../Attributes/data/unsafe_links_blocked.html | 1 +
+ .../Attributes/data/unsafe_links_blocked.md | 5 ++
+ .../Attributes/Util/AttributesHelperTest.php | 66 ++++++++++++++++++++++
+ 12 files changed, 162 insertions(+), 4 deletions(-)
+ create mode 100644 tests/functional/Extension/Attributes/data/allowlist.html
+ create mode 100644 tests/functional/Extension/Attributes/data/allowlist.md
+ create mode 100644 tests/functional/Extension/Attributes/data/js_event_attributes.html
+ create mode 100644 tests/functional/Extension/Attributes/data/js_event_attributes.md
+ create mode 100644 tests/functional/Extension/Attributes/data/unsafe_links_allowed.html
+ create mode 100644 tests/functional/Extension/Attributes/data/unsafe_links_allowed.md
+ create mode 100644 tests/functional/Extension/Attributes/data/unsafe_links_blocked.html
+ create mode 100644 tests/functional/Extension/Attributes/data/unsafe_links_blocked.md
+
+diff --git a/src/Extension/Attributes/AttributesExtension.php b/src/Extension/Attributes/AttributesExtension.php
+index 2ef3d85..b29606d 100644
+--- a/src/Extension/Attributes/AttributesExtension.php
++++ b/src/Extension/Attributes/AttributesExtension.php
+@@ -19,14 +19,26 @@ use League\CommonMark\Event\DocumentParsedEvent;
+ 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']);
+ }
+ }
+diff --git a/src/Extension/Attributes/Event/AttributesListener.php b/src/Extension/Attributes/Event/AttributesListener.php
+index feec8cc..fa068f4 100644
+--- a/src/Extension/Attributes/Event/AttributesListener.php
++++ b/src/Extension/Attributes/Event/AttributesListener.php
+@@ -29,6 +29,19 @@ final class AttributesListener
+ private const DIRECTION_PREFIX = 'prefix';
+ private const DIRECTION_SUFFIX = 'suffix';
+
++ /** @var list<string> */
++ private array $allowList;
++ private bool $allowUnsafeLinks;
++
++ /**
++ * @param list<string> $allowList
++ */
++ public function __construct(array $allowList = [], bool $allowUnsafeLinks = true)
++ {
++ $this->allowList = $allowList;
++ $this->allowUnsafeLinks = $allowUnsafeLinks;
++ }
++
+ public function processDocument(DocumentParsedEvent $event): void
+ {
+ foreach ($event->getDocument()->iterator() as $node) {
+@@ -50,7 +63,7 @@ final class AttributesListener
+ $attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target);
+ }
+
+- $target->data->set('attributes', $attributes);
++ $target->data->set('attributes', AttributesHelper::filterAttributes($attributes, $this->allowList, $this->allowUnsafeLinks));
+ }
+
+ $node->detach();
+diff --git a/src/Extension/Attributes/Util/AttributesHelper.php b/src/Extension/Attributes/Util/AttributesHelper.php
+index de5c111..33d3a0a 100644
+--- a/src/Extension/Attributes/Util/AttributesHelper.php
++++ b/src/Extension/Attributes/Util/AttributesHelper.php
+@@ -134,4 +134,42 @@ final class AttributesHelper
+
+ return $attributes;
+ }
++
++ /**
++ * @param array<string, mixed> $attributes
++ * @param list<string> $allowList
++ *
++ * @return array<string, mixed>
++ */
++ public static function filterAttributes(array $attributes, array $allowList, bool $allowUnsafeLinks): array
++ {
++ $allowList = \array_fill_keys($allowList, true);
++
++ foreach ($attributes as $name => $value) {
++ $attrNameLower = \strtolower($name);
++
++ // Remove any unsafe links
++ if (! $allowUnsafeLinks && ($attrNameLower === 'href' || $attrNameLower === 'src') && \is_string($value) && RegexHelper::isLinkPotentiallyUnsafe($value)) {
++ unset($attributes[$name]);
++ continue;
++ }
++
++ // No allowlist?
++ if ($allowList === []) {
++ // Just remove JS event handlers
++ if (\str_starts_with($attrNameLower, 'on')) {
++ unset($attributes[$name]);
++ }
++
++ continue;
++ }
++
++ // Remove any attributes not in that allowlist (case-sensitive)
++ if (! isset($allowList[$name])) {
++ unset($attributes[$name]);
++ }
++ }
++
++ return $attributes;
++ }
+ }
+diff --git a/tests/functional/Extension/Attributes/data/allowlist.html b/tests/functional/Extension/Attributes/data/allowlist.html
+new file mode 100644
+index 0000000..92ab9de
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/allowlist.html
+@@ -0,0 +1,3 @@
++<h2 id="header1">Header with attributes</h2>
++<p class="text">some text</p>
++<p><img align="left" src="/assets/image.jpg" alt="image" /></p>
+diff --git a/tests/functional/Extension/Attributes/data/allowlist.md b/tests/functional/Extension/Attributes/data/allowlist.md
+new file mode 100644
+index 0000000..996c647
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/allowlist.md
+@@ -0,0 +1,12 @@
++---
++attributes:
++ allow: [id, class, align]
++---
++
++Header with attributes {#header1}
++---------------------------------
++
++{class="text" hello="world"}
++some text
++
++{align=left width=100px}
+diff --git a/tests/functional/Extension/Attributes/data/js_event_attributes.html b/tests/functional/Extension/Attributes/data/js_event_attributes.html
+new file mode 100644
+index 0000000..667c05e
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/js_event_attributes.html
+@@ -0,0 +1 @@
++<p><img class="blocked" src="" alt="this extension blocks js event attributes" /></p>
+diff --git a/tests/functional/Extension/Attributes/data/js_event_attributes.md b/tests/functional/Extension/Attributes/data/js_event_attributes.md
+new file mode 100644
+index 0000000..e0a0642
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/js_event_attributes.md
+@@ -0,0 +1 @@
++![this extension blocks js event attributes](){onerror=alert(1) class=blocked}
+diff --git a/tests/functional/Extension/Attributes/data/unsafe_links_allowed.html b/tests/functional/Extension/Attributes/data/unsafe_links_allowed.html
+new file mode 100644
+index 0000000..3497e89
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/unsafe_links_allowed.html
+@@ -0,0 +1 @@
++<p><a href="javascript:alert(1)">click me</a></p>
+diff --git a/tests/functional/Extension/Attributes/data/unsafe_links_allowed.md b/tests/functional/Extension/Attributes/data/unsafe_links_allowed.md
+new file mode 100644
+index 0000000..9a1d45e
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/unsafe_links_allowed.md
+@@ -0,0 +1,5 @@
++---
++allow_unsafe_links: true
++---
++
++[click me](javascript:alert(1)){href="javascript:alert(1)"}
+diff --git a/tests/functional/Extension/Attributes/data/unsafe_links_blocked.html b/tests/functional/Extension/Attributes/data/unsafe_links_blocked.html
+new file mode 100644
+index 0000000..0205a6b
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/unsafe_links_blocked.html
+@@ -0,0 +1 @@
++<p><a>click me</a></p>
+diff --git a/tests/functional/Extension/Attributes/data/unsafe_links_blocked.md b/tests/functional/Extension/Attributes/data/unsafe_links_blocked.md
+new file mode 100644
+index 0000000..5c706a3
+--- /dev/null
++++ b/tests/functional/Extension/Attributes/data/unsafe_links_blocked.md
+@@ -0,0 +1,5 @@
++---
++allow_unsafe_links: false
++---
++
++[click me](javascript:alert(1)){href="javascript:alert(1)"}
+diff --git a/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php b/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php
+index 17a4653..67767c5 100644
+--- a/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php
++++ b/tests/unit/Extension/Attributes/Util/AttributesHelperTest.php
+@@ -182,4 +182,70 @@ final class AttributesHelperTest extends TestCase
+ ['id' => 'block', 'class' => 'inline block'],
+ ];
+ }
++
++ /**
++ * @dataProvider dataForTestFilterAttributes
++ *
++ * @param array<string, mixed> $attributes
++ * @param list<string> $allowList
++ * @param array<string, mixed> $expected
++ */
++ public function testFilterAttributes(array $attributes, array $allowList, bool $allowUnsafeLinks, array $expected): void
++ {
++ $this->assertEquals($expected, AttributesHelper::filterAttributes($attributes, $allowList, $allowUnsafeLinks));
++ }
++
++ /**
++ * @return iterable<array<mixed>>
++ */
++ public static function dataForTestFilterAttributes(): iterable
++ {
++ // No allow list; unsafe links disallowed (default behavior)
++ yield [
++ ['id' => 'foo', 'class' => 'bar', 'onclick' => 'alert("XSS")', 'href' => 'javascript:alert("XSS")'],
++ [],
++ false,
++ ['id' => 'foo', 'class' => 'bar'],
++ ];
++
++ // No allow list; unsafe links allowed
++ yield [
++ ['id' => 'foo', 'class' => 'bar', 'onclick' => 'alert("XSS")', 'href' => 'javascript:alert("XSS")'],
++ [],
++ true,
++ ['id' => 'foo', 'class' => 'bar', 'href' => 'javascript:alert("XSS")'],
++ ];
++
++ // Allow list; unsafe links disallowed
++ yield [
++ ['id' => 'foo', 'class' => 'bar', 'onclick' => 'alert("XSS")', 'href' => 'javascript:alert("XSS")'],
++ ['id', 'onclick', 'href'],
++ false,
++ ['id' => 'foo', 'onclick' => 'alert("XSS")'],
++ ];
++
++ // Allow list; unsafe links allowed
++ yield [
++ ['id' => 'foo', 'class' => 'bar', 'onclick' => 'alert("XSS")', 'href' => 'javascript:alert("XSS")'],
++ ['id', 'onclick', 'href'],
++ true,
++ ['id' => 'foo', 'onclick' => 'alert("XSS")', 'href' => 'javascript:alert("XSS")'],
++ ];
++
++ // Allow list blocks all
++ yield [
++ ['id' => 'foo', 'class' => '<script>alert("XSS")</script>'],
++ ['style'],
++ false,
++ [],
++ ];
++
++ // Can't use weird casing to bypass allowlist or 'on*' restriction
++ yield [
++ ['ID' => 'foo', 'oNcLiCk' => 'alert("XSS")'],
++ ['id', 'class'],
++ false,
++ [],
++ ];
++ }
+ }
diff -Nru php-league-commonmark-2.3.9/debian/patches/0003-Add-regression-test.patch php-league-commonmark-2.3.9/debian/patches/0003-Add-regression-test.patch
--- php-league-commonmark-2.3.9/debian/patches/0003-Add-regression-test.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/patches/0003-Add-regression-test.patch 2026-03-26 08:15:50.000000000 +0100
@@ -0,0 +1,42 @@
+From: Colin O'Dell <[email protected]>
+Date: Thu, 5 Mar 2026 07:22:58 -0500
+Subject: Add regression test
+
+Origin: upstream, https://github.com/thephpleague/commonmark/commit/f6e74434dd1a91f195f80cb0184b746a4187272a
+---
+ .../DisallowedRawHtml/DisallowedRawHtmlRendererTest.php | 15 +++++++++++++++
+ 1 file changed, 15 insertions(+)
+
+diff --git a/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php b/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php
+index 968d780..af25950 100644
+--- a/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php
++++ b/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php
+@@ -71,6 +71,16 @@ final class DisallowedRawHtmlRendererTest extends TestCase
+ yield ['<script>', '<script>'];
+ yield ['<plaintext>', '<plaintext>'];
+
++ // Newline/whitespace bypass attempts (security fix)
++ yield ["<script >", "<script >"];
++ yield ["<script\n>", "<script\n>"];
++ yield ["<script\t>", "<script\t>"];
++ yield ["<script\r\n>", "<script\r\n>"];
++ yield ["<iframe\nwidth=\"560\">", "<iframe\nwidth=\"560\">"];
++
++ // Ensure non-disallowed tags with similar names are NOT filtered
++ yield ['<scriptfoo>', '<scriptfoo>'];
++
+ // Tags not escaped by default
+ yield ['<strong>', '<strong>'];
+ }
+@@ -107,6 +117,11 @@ final class DisallowedRawHtmlRendererTest extends TestCase
+ yield ['<strong/>', '<strong/>'];
+ yield ['<strong />', '<strong />'];
+
++ // Newline bypass with custom config
++ yield ["<strong >", "<strong >"];
++ yield ["<strong\n>", "<strong\n>"];
++ yield ["<strong\t>", "<strong\t>"];
++
+ // Defaults that I didn't include in my custom config
+ yield ['<title>', '<title>'];
+ yield ['<textarea>', '<textarea>'];
diff -Nru php-league-commonmark-2.3.9/debian/patches/0004-Fix-DisallowedRawHtml-bypass-via-newline-tab-in-tag-.patch php-league-commonmark-2.3.9/debian/patches/0004-Fix-DisallowedRawHtml-bypass-via-newline-tab-in-tag-.patch
--- php-league-commonmark-2.3.9/debian/patches/0004-Fix-DisallowedRawHtml-bypass-via-newline-tab-in-tag-.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/patches/0004-Fix-DisallowedRawHtml-bypass-via-newline-tab-in-tag-.patch 2026-03-26 08:15:50.000000000 +0100
@@ -0,0 +1,47 @@
+From: Colin O'Dell <[email protected]>
+Date: Thu, 5 Mar 2026 07:43:00 -0500
+Subject: Fix DisallowedRawHtml bypass via newline/tab in tag names
+
+Origin: upstream, https://github.com/thephpleague/commonmark/commit/5c0c4c8fe5a31e8260be99e0afad7136a27c79e6
+Bug: https://github.com/thephpleague/commonmark/security/advisories/GHSA-4v6x-c7xx-hw9f
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2026-30838
+---
+ src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php | 2 +-
+ .../Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php | 4 ++--
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php b/src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php
+index 06252a3..2bcb89a 100644
+--- a/src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php
++++ b/src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php
+@@ -45,7 +45,7 @@ final class DisallowedRawHtmlRenderer implements NodeRendererInterface, Configur
+ return $rendered;
+ }
+
+- $regex = \sprintf('/<(\/?(?:%s)[ \/>])/i', \implode('|', \array_map('preg_quote', $tags)));
++ $regex = \sprintf('/<(\/?(?:%s)[\s\/>])/i', \implode('|', \array_map('preg_quote', $tags)));
+
+ // Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
+ return \preg_replace($regex, '<$1', $rendered);
+diff --git a/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php b/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php
+index af25950..f8866e3 100644
+--- a/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php
++++ b/tests/unit/Extension/DisallowedRawHtml/DisallowedRawHtmlRendererTest.php
+@@ -72,7 +72,7 @@ final class DisallowedRawHtmlRendererTest extends TestCase
+ yield ['<plaintext>', '<plaintext>'];
+
+ // Newline/whitespace bypass attempts (security fix)
+- yield ["<script >", "<script >"];
++ yield ['<script >', '<script >'];
+ yield ["<script\n>", "<script\n>"];
+ yield ["<script\t>", "<script\t>"];
+ yield ["<script\r\n>", "<script\r\n>"];
+@@ -118,7 +118,7 @@ final class DisallowedRawHtmlRendererTest extends TestCase
+ yield ['<strong />', '<strong />'];
+
+ // Newline bypass with custom config
+- yield ["<strong >", "<strong >"];
++ yield ['<strong >', '<strong >'];
+ yield ["<strong\n>", "<strong\n>"];
+ yield ["<strong\t>", "<strong\t>"];
+
diff -Nru php-league-commonmark-2.3.9/debian/patches/0005-Fix-DomainFilteringAdapter-hostname-boundary-bypass.patch php-league-commonmark-2.3.9/debian/patches/0005-Fix-DomainFilteringAdapter-hostname-boundary-bypass.patch
--- php-league-commonmark-2.3.9/debian/patches/0005-Fix-DomainFilteringAdapter-hostname-boundary-bypass.patch 1970-01-01 01:00:00.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/patches/0005-Fix-DomainFilteringAdapter-hostname-boundary-bypass.patch 2026-03-26 08:15:50.000000000 +0100
@@ -0,0 +1,106 @@
+From: Colin O'Dell <[email protected]>
+Date: Thu, 19 Mar 2026 09:01:30 -0400
+Subject: Fix DomainFilteringAdapter hostname boundary bypass
+
+Origin: backport, https://github.com/thephpleague/commonmark/commit/59fb075d2101740c337c7216e3f32b36c204218b
+Bug: https://github.com/thephpleague/commonmark/security/advisories/GHSA-hh8v-hgvp-g3f5
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2026-33347
+---
+ src/Extension/Embed/DomainFilteringAdapter.php | 38 ++++++++++++++--------
+ .../Extension/Embed/DomainFilteringAdapterTest.php | 18 ++++++++--
+ 2 files changed, 41 insertions(+), 15 deletions(-)
+
+diff --git a/src/Extension/Embed/DomainFilteringAdapter.php b/src/Extension/Embed/DomainFilteringAdapter.php
+index 69dc096..b00b028 100644
+--- a/src/Extension/Embed/DomainFilteringAdapter.php
++++ b/src/Extension/Embed/DomainFilteringAdapter.php
+@@ -17,15 +17,16 @@ class DomainFilteringAdapter implements EmbedAdapterInterface
+ {
+ private EmbedAdapterInterface $decorated;
+
+- private string $regex;
++ /** @var string[] */
++ private array $allowedDomains;
+
+ /**
+ * @param string[] $allowedDomains
+ */
+ public function __construct(EmbedAdapterInterface $decorated, array $allowedDomains)
+ {
+- $this->decorated = $decorated;
+- $this->regex = self::createRegex($allowedDomains);
++ $this->decorated = $decorated;
++ $this->allowedDomains = \array_map('strtolower', $allowedDomains);
+ }
+
+ /**
+@@ -33,18 +34,29 @@ class DomainFilteringAdapter implements EmbedAdapterInterface
+ */
+ public function updateEmbeds(array $embeds): void
+ {
+- $this->decorated->updateEmbeds(\array_values(\array_filter($embeds, function (Embed $embed): bool {
+- return \preg_match($this->regex, $embed->getUrl()) === 1;
+- })));
++ $this->decorated->updateEmbeds(\array_values(\array_filter($embeds, [$this, 'isAllowed'])));
+ }
+
+- /**
+- * @param string[] $allowedDomains
+- */
+- private static function createRegex(array $allowedDomains): string
++ private function isAllowed(Embed $embed): bool
+ {
+- $allowedDomains = \array_map('preg_quote', $allowedDomains);
+-
+- return '/^(?:https?:\/\/)?(?:[^.]+\.)*(' . \implode('|', $allowedDomains) . ')/';
++ $url = $embed->getUrl();
++ $scheme = \parse_url($url, \PHP_URL_SCHEME);
++ if ($scheme === null || $scheme === false) {
++ // Bare domain (no scheme) - assume https:// so parse_url can extract the host
++ $url = 'https://' . $url;
++ } elseif (\strtolower($scheme) !== 'http' && \strtolower($scheme) !== 'https') {
++ return false;
++ }
++
++ $host = \parse_url($url, \PHP_URL_HOST);
++ $host = \strtolower(\rtrim((string) $host, '.'));
++
++ foreach ($this->allowedDomains as $domain) {
++ if ($host === $domain || \str_ends_with($host, '.' . $domain)) {
++ return true;
++ }
++ }
++
++ return false;
+ }
+ }
+diff --git a/tests/unit/Extension/Embed/DomainFilteringAdapterTest.php b/tests/unit/Extension/Embed/DomainFilteringAdapterTest.php
+index 436e398..d320eb7 100644
+--- a/tests/unit/Extension/Embed/DomainFilteringAdapterTest.php
++++ b/tests/unit/Extension/Embed/DomainFilteringAdapterTest.php
+@@ -28,9 +28,23 @@ final class DomainFilteringAdapterTest extends TestCase
+ $embed2 = new Embed('foo.example.com'),
+ new Embed('www.bar.com'),
+ new Embed('badexample.com'),
+- $embed3 = new Embed('http://foo.bar.com'),
+- $embed4 = new Embed('https://foo.bar.com/baz'),
++ $embed3 = new Embed('HTTP://foo.bar.com'),
++ $embed4 = new Embed('hTtPs://foo.bar.com/baz'),
+ new Embed('https://bar.com'),
++ new Embed('https://example.com.evil'),
++ new Embed('https://example.com.evil/path'),
++ new Embed('https://foo.bar.com.evil'),
++ new Embed('example.com.evil'),
++ new Embed('example.com.evil/path'),
++ new Embed('foo.bar.com.evil'),
++ new Embed('https://[email protected]'),
++ new Embed('https://user:[email protected]'),
++ new Embed('https://example.com:[email protected]/path'),
++ new Embed('javascript:alert(1)'),
++ new Embed('ftp://example.com'),
++ new Embed('file:///etc/passwd'),
++ new Embed('data:text/html,<script>alert(1)</script>'),
++ new Embed('//example.com/path'),
+ ];
+
+ $inner = $this->createMock(EmbedAdapterInterface::class);
diff -Nru php-league-commonmark-2.3.9/debian/patches/series php-league-commonmark-2.3.9/debian/patches/series
--- php-league-commonmark-2.3.9/debian/patches/series 2023-02-15 18:36:26.000000000 +0100
+++ php-league-commonmark-2.3.9/debian/patches/series 2026-03-26 08:15:50.000000000 +0100
@@ -1 +1,5 @@
0001-Skip-tests-relying-on-packages-unavailable-in-Debian.patch
+0002-Fix-XSS-in-AttributesExtension.patch
+0003-Add-regression-test.patch
+0004-Fix-DisallowedRawHtml-bypass-via-newline-tab-in-tag-.patch
+0005-Fix-DomainFilteringAdapter-hostname-boundary-bypass.patch
signature.asc
Description: PGP signature
--- End Message ---