Skip to content

Commit 17df023

Browse files
committed
Merge branch '2.8'
2 parents 238191e + 59fb075 commit 17df023

File tree

4 files changed

+54
-21
lines changed

4 files changed

+54
-21
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
66

77
## [Unreleased][unreleased]
88

9+
## [2.8.2] - 2026-03-19
10+
11+
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.
12+
13+
### Fixed
14+
- Fixed `DomainFilteringAdapter` hostname boundary bypass where domains like `youtube.com.evil` could match an allowlist entry for `youtube.com` (GHSA-hh8v-hgvp-g3f5)
15+
916
## [2.8.1] - 2026-03-05
1017

1118
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.
725732
- Alternative 1: Use `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` if you don't need to customize the environment
726733
- Alternative 2: Instantiate a new `Environment` and add the necessary extensions yourself
727734

728-
[unreleased]: https://github.com/thephpleague/commonmark/compare/2.8.1...HEAD
735+
[unreleased]: https://github.com/thephpleague/commonmark/compare/2.8.2...HEAD
736+
[2.8.2]: https://github.com/thephpleague/commonmark/compare/2.8.1...2.8.2
729737
[2.8.1]: https://github.com/thephpleague/commonmark/compare/2.8.0...2.8.1
730738
[2.8.0]: https://github.com/thephpleague/commonmark/compare/2.7.1...2.8.0
731739
[2.7.1]: https://github.com/thephpleague/commonmark/compare/2.7.0...2.7.1

docs/2.x/extensions/front-matter.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ composer require league/commonmark
2828

2929
See the [installation](/2.x/installation/) section for more details.
3030

31-
You will also need to install `symfony/yaml` or the [YAML extension for PHP](https://www.php.net/manual/book.yaml.php) to use this extension. For `symfony/yaml`:
31+
You will also need to install `symfony/yaml` (2.6 or higher) or the [YAML extension for PHP](https://www.php.net/manual/book.yaml.php) to use this extension. For `symfony/yaml`:
3232

3333
```bash
3434
composer require symfony/yaml
3535
```
3636

37-
(You can use any version of `symfony/yaml` 2.6 or higher, though we recommend using 4.0 or higher.)
37+
If both are installed, the PHP YAML extension will be used by default.
38+
39+
**Warning:** When using the PHP YAML extension, avoid setting `yaml.decode_php=1` in your `php.ini` file as this enables deserialization of arbitrary classes, which can lead to security vulnerabilities!
3840

3941
## Front Matter Syntax
4042

src/Extension/Embed/DomainFilteringAdapter.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,46 @@ class DomainFilteringAdapter implements EmbedAdapterInterface
1717
{
1818
private EmbedAdapterInterface $decorated;
1919

20-
/** @psalm-var non-empty-string */
21-
private string $regex;
20+
/** @var string[] */
21+
private array $allowedDomains;
2222

2323
/**
2424
* @param string[] $allowedDomains
2525
*/
2626
public function __construct(EmbedAdapterInterface $decorated, array $allowedDomains)
2727
{
28-
$this->decorated = $decorated;
29-
$this->regex = self::createRegex($allowedDomains);
28+
$this->decorated = $decorated;
29+
$this->allowedDomains = \array_map('strtolower', $allowedDomains);
3030
}
3131

3232
/**
3333
* {@inheritDoc}
3434
*/
3535
public function updateEmbeds(array $embeds): void
3636
{
37-
$this->decorated->updateEmbeds(\array_values(\array_filter($embeds, function (Embed $embed): bool {
38-
return \preg_match($this->regex, $embed->getUrl()) === 1;
39-
})));
37+
$this->decorated->updateEmbeds(\array_values(\array_filter($embeds, [$this, 'isAllowed'])));
4038
}
4139

42-
/**
43-
* @param string[] $allowedDomains
44-
*
45-
* @psalm-return non-empty-string
46-
*/
47-
private static function createRegex(array $allowedDomains): string
40+
private function isAllowed(Embed $embed): bool
4841
{
49-
$allowedDomains = \array_map('preg_quote', $allowedDomains);
50-
51-
return '/^(?:https?:\/\/)?(?:[^.]+\.)*(' . \implode('|', $allowedDomains) . ')/';
42+
$url = $embed->getUrl();
43+
$scheme = \parse_url($url, \PHP_URL_SCHEME);
44+
if ($scheme === null || $scheme === false) {
45+
// Bare domain (no scheme) - assume https:// so parse_url can extract the host
46+
$url = 'https://' . $url;
47+
} elseif (\strtolower($scheme) !== 'http' && \strtolower($scheme) !== 'https') {
48+
return false;
49+
}
50+
51+
$host = \parse_url($url, \PHP_URL_HOST);
52+
$host = \strtolower(\rtrim((string) $host, '.'));
53+
54+
foreach ($this->allowedDomains as $domain) {
55+
if ($host === $domain || \str_ends_with($host, '.' . $domain)) {
56+
return true;
57+
}
58+
}
59+
60+
return false;
5261
}
5362
}

tests/unit/Extension/Embed/DomainFilteringAdapterTest.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,23 @@ public function testUpdateEmbeds(): void
2828
$embed2 = new Embed('foo.example.com'),
2929
new Embed('www.bar.com'),
3030
new Embed('badexample.com'),
31-
$embed3 = new Embed('http://foo.bar.com'),
32-
$embed4 = new Embed('https://foo.bar.com/baz'),
31+
$embed3 = new Embed('HTTP://foo.bar.com'),
32+
$embed4 = new Embed('hTtPs://foo.bar.com/baz'),
3333
new Embed('https://bar.com'),
34+
new Embed('https://example.com.evil'),
35+
new Embed('https://example.com.evil/path'),
36+
new Embed('https://foo.bar.com.evil'),
37+
new Embed('example.com.evil'),
38+
new Embed('example.com.evil/path'),
39+
new Embed('foo.bar.com.evil'),
40+
new Embed('https://example.com@evil.com'),
41+
new Embed('https://user:pass@evil.com'),
42+
new Embed('https://example.com:pass@evil.com/path'),
43+
new Embed('javascript:alert(1)'),
44+
new Embed('ftp://example.com'),
45+
new Embed('file:///etc/passwd'),
46+
new Embed('data:text/html,<script>alert(1)</script>'),
47+
new Embed('//example.com/path'),
3448
];
3549

3650
$inner = $this->createMock(EmbedAdapterInterface::class);

0 commit comments

Comments
 (0)