VYPR
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.

PackageAffected versionsPatched versions
league/commonmarkPackagist
>= 2.3.0, < 2.8.22.8.2

Affected products

1

Patches

1
59fb075d2101

Fix DomainFilteringAdapter hostname boundary bypass

https://github.com/thephpleague/commonmarkColin O'DellMar 19, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.