fix: only linkify http and https URIs on the error page

escapeButPreserveUris() matched the scheme with "[A-z]+?://", which accepts
any scheme. A frame comment containing javascript://x.co/?%0Aalert(1) survives
htmlspecialchars() untouched and was then wrapped in an anchor, so clicking it
executed script in the origin of the error page. "[A-z]" is also a code point
range rather than a letter class, so it additionally matched the six characters
between "Z" and "a", making "_://x.co/a" a link too.

Match the scheme with "https?" instead, case-insensitively so uppercase
"HTTP://" keeps working. Other schemes stay as escaped plain text. The editor
integration links are not affected, they are emitted directly by the template
and never pass through this method.

Fixes #791
This commit is contained in:
dualfroz committed 2026-09-03 14:14:10 +02:00
1 parent 238f0d6b9a
commit 8652196c72
2 files changed
+48 -1

No files matched your search

+5 -1
View File
@@ -80,6 +80,10 @@ class TemplateHelper
* Escapes a string for output in an HTML document, but preserves
* URIs within it, and converts them to clickable anchor elements.
*
* Only http and https URIs are turned into links. Any other scheme is left as
* plain text, because schemes such as javascript: and data: would otherwise
* become clickable script execution on the error page.
*
* @param string $raw
* @return string
*/
@@ -87,7 +91,7 @@ class TemplateHelper
{
$escaped = $this->escape($raw);
return preg_replace(
"@([A-z]+?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@",
"@(https?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@i",
"<a href=\"$1\" target=\"_blank\" rel=\"noreferrer noopener\">$1</a>",
$escaped
);
+43
View File
@@ -63,6 +63,49 @@ class TemplateHelperTest extends TestCase
);
}
/**
* @covers Whoops\Util\TemplateHelper::escapeButPreserveUris
*/
public function testEscapeButPreserveUrisIgnoresNonHttpSchemes()
{
// None of these contain HTML special characters, so escaping leaves them
// untouched and the only question is whether they get turned into links.
$originals = [
// Clicking such a link would run alert(1): the browser decodes %0A to a
// newline, which ends the "//x.co/?" JavaScript comment.
'javascript://x.co/?%0Aalert(1)',
'JAVASCRIPT://x.co/?%0Aalert(1)',
'vbscript://x.co/?%0Amsgbox(1)',
'data://text.html/?%0Ax',
// "_" is between "Z" and "a" in ASCII, so it used to pass as a scheme.
'_://x.co/a',
'See javascript://x.co/?%0Aalert(document.cookie) for details',
];
foreach ($originals as $original) {
$this->assertSame($original, $this->helper->escapeButPreserveUris($original));
}
}
/**
* @covers Whoops\Util\TemplateHelper::escapeButPreserveUris
*/
public function testEscapeButPreserveUrisStillLinksHttpUris()
{
$originals = [
'http://example.com/a',
'https://example.com/a',
'HTTPS://example.com/a',
];
foreach ($originals as $original) {
$this->assertSame(
"<a href=\"$original\" target=\"_blank\" rel=\"noreferrer noopener\">$original</a>",
$this->helper->escapeButPreserveUris($original)
);
}
}
/**
* @covers Whoops\Util\TemplateHelper::breakOnDelimiter
*/