Medium severity6.1NVD Advisory· Published Mar 24, 2026· Updated Apr 8, 2026
CVE-2026-33347
CVE-2026-33347
Description
league/commonmark is a PHP Markdown parser. From version 2.3.0 to before version 2.8.2, the DomainFilteringAdapter in the Embed extension is vulnerable to an allowlist bypass due to a missing hostname boundary assertion in the domain-matching regex. An attacker-controlled domain like youtube.com.evil passes the allowlist check when youtube.com is an allowed domain. This issue has been patched in version 2.8.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
league/commonmarkPackagist | >= 2.3.0, < 2.8.2 | 2.8.2 |
Affected products
1Patches
159fb075d2101Fix DomainFilteringAdapter hostname boundary bypass
3 files changed · +50 −19
CHANGELOG.md+9 −1 modified@@ -6,6 +6,13 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi ## [Unreleased][unreleased] +## [2.8.2] - 2026-03-19 + +This is a **security release** to address an issue where the `allowed_domains` setting for the `Embed` extension can be bypassed, resulting in a possible SSRF and XSS vulnerabilities. + +### Fixed +- Fixed `DomainFilteringAdapter` hostname boundary bypass where domains like `youtube.com.evil` could match an allowlist entry for `youtube.com` (GHSA-hh8v-hgvp-g3f5) + ## [2.8.1] - 2026-03-05 This is a **security release** to address an issue where `DisallowedRawHtml` can be bypassed, resulting in a possible cross-site scripting (XSS) vulnerability. @@ -725,7 +732,8 @@ No changes were introduced since the previous release. - Alternative 1: Use `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` if you don't need to customize the environment - Alternative 2: Instantiate a new `Environment` and add the necessary extensions yourself -[unreleased]: https://github.com/thephpleague/commonmark/compare/2.8.1...HEAD +[unreleased]: https://github.com/thephpleague/commonmark/compare/2.8.2...HEAD +[2.8.2]: https://github.com/thephpleague/commonmark/compare/2.8.1...2.8.2 [2.8.1]: https://github.com/thephpleague/commonmark/compare/2.8.0...2.8.1 [2.8.0]: https://github.com/thephpleague/commonmark/compare/2.7.1...2.8.0 [2.7.1]: https://github.com/thephpleague/commonmark/compare/2.7.0...2.7.1
src/Extension/Embed/DomainFilteringAdapter.php+25 −16 modified@@ -17,37 +17,46 @@ class DomainFilteringAdapter implements EmbedAdapterInterface { private EmbedAdapterInterface $decorated; - /** @psalm-var non-empty-string */ - 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); } /** * {@inheritDoc} */ 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 - * - * @psalm-return non-empty-string - */ - 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; } }
tests/unit/Extension/Embed/DomainFilteringAdapterTest.php+16 −2 modified@@ -28,9 +28,23 @@ public function testUpdateEmbeds(): void $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://example.com@evil.com'), + new Embed('https://user:pass@evil.com'), + new Embed('https://example.com:pass@evil.com/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);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/thephpleague/commonmark/commit/59fb075d2101740c337c7216e3f32b36c204218bnvdPatchWEB
- github.com/advisories/GHSA-hh8v-hgvp-g3f5ghsaADVISORY
- github.com/thephpleague/commonmark/security/advisories/GHSA-hh8v-hgvp-g3f5nvdMitigationVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-33347ghsaADVISORY
- github.com/thephpleague/commonmark/releases/tag/2.8.2nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.