Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/Sanitize.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ public function replace_urls(DOMDocument $document, string $tag, $attributes)
$value = $this->registry->call(Misc::class, 'absolutize_url', [$element->getAttribute($attribute), $this->base]);
if ($value !== false) {
// Block disallowed URI protocols (e.g. javascript:), otherwise force HTTPS where applicable
$value = ($this->disallowed_uri_schemes !== [] && !$this->is_allowed_scheme($value))
$value = ($this->disallowed_uri_schemes !== [] && !$this->is_allowed_uri($tag, $attribute, $value))
? 'unsafe:' . $value
: $this->https_url($value);
$element->setAttribute($attribute, $value);
Expand Down Expand Up @@ -779,6 +779,36 @@ public function is_allowed_scheme(string $uri): bool
return !in_array($scheme, $this->disallowed_uri_schemes, true);
}

/**
* Elements/attributes on which a `data:` URI carrying an image is still allowed
* even when the `data` scheme is otherwise disallowed. Such URIs are inert
* (an `<img>` never executes script), whereas `data:` on navigational or framing
* attributes (`<a href>`, `<iframe src>`, ...) may load attacker-controlled HTML.
* @var array<string, list<string>>
*/
private const DATA_IMAGE_URI_ALLOWED = [
'image' => ['src'],
'img' => ['src'],
'video' => ['poster'],
];

/**
* Like {@see is_allowed_scheme()}, but aware of the element and attribute the URI sits on,
* so that `data:image/...` can stay allowed on image-bearing attributes while `data:` is
* blocked elsewhere (e.g. `<a href="data:text/html,...">`, `<iframe src="data:...">`).
*/
public function is_allowed_uri(string $tag, string $attribute, string $uri): bool
{
$pos = strpos($uri, ':');
if ($pos !== false && strtolower(substr($uri, 0, $pos)) === 'data'
&& in_array('data', $this->disallowed_uri_schemes, true)
&& in_array($attribute, self::DATA_IMAGE_URI_ALLOWED[strtolower($tag)] ?? [], true)
&& preg_match('%^data:image/[a-z0-9.+-]+[;,]%i', $uri) === 1) {
return true;
}
return $this->is_allowed_scheme($uri);
}

/**
* Block disallowed URI schemes (protocols) on elements carrying an href attribute.
*
Expand Down
42 changes: 42 additions & 0 deletions tests/Unit/SanitizeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,48 @@ public static function disallowedUriSchemesProvider(): iterable
'<svg xmlns:xlink="http://www.w3.org/1999/xlink"><a xlink:href="https://example.com/page">x</a></svg>',
['javascript'],
];

yield 'data scheme in href' => [
'<a href="data:text/html,<script>alert(1)</script>">Click me</a>',
'<a href="unsafe:data:text/html,%3Cscript%3Ealert(1)%3C/script%3E">Click me</a>',
['javascript', 'data'],
];

yield 'data scheme in iframe src' => [
'<iframe src="data:text/html,x"></iframe>',
'<iframe src="unsafe:data:text/html,x" sandbox="allow-scripts allow-same-origin"></iframe>',
['javascript', 'data'],
];

yield 'data scheme case insensitive' => [
'<a href="DATA:text/html,x">Click me</a>',
'<a href="unsafe:data:text/html,x">Click me</a>',
['javascript', 'data'],
];

yield 'data image kept on img src' => [
'<img src="data:image/png;base64,iVBORw0KGgo=">',
'<img src="data:image/png;base64,iVBORw0KGgo=">',
['javascript', 'data'],
];

yield 'data non-image blocked on img src' => [
'<img src="data:text/html,x">',
'<img src="unsafe:data:text/html,x">',
['javascript', 'data'],
];

yield 'data image blocked on href' => [
'<a href="data:image/png;base64,iVBORw0KGgo=">Click me</a>',
'<a href="unsafe:data:image/png;base64,iVBORw0KGgo=">Click me</a>',
['javascript', 'data'],
];

yield 'data scheme unaffected when not disallowed' => [
'<a href="data:text/html,x">Click me</a>',
'<a href="data:text/html,x">Click me</a>',
['javascript'],
];
}

/**
Expand Down
Loading