From 8652196c7240917ffc943b44aa818e5ec241740e Mon Sep 17 00:00:00 2001 From: dualfroz Date: Thu, 3 Sep 2026 14:14:10 +0200 Subject: [PATCH] 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 --- src/Whoops/Util/TemplateHelper.php | 6 +++- tests/Whoops/Util/TemplateHelperTest.php | 43 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Whoops/Util/TemplateHelper.php b/src/Whoops/Util/TemplateHelper.php index 5612c0b..c5913eb 100644 --- a/src/Whoops/Util/TemplateHelper.php +++ b/src/Whoops/Util/TemplateHelper.php @@ -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", "$1", $escaped ); diff --git a/tests/Whoops/Util/TemplateHelperTest.php b/tests/Whoops/Util/TemplateHelperTest.php index 086645a..75baf7a 100644 --- a/tests/Whoops/Util/TemplateHelperTest.php +++ b/tests/Whoops/Util/TemplateHelperTest.php @@ -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( + "$original", + $this->helper->escapeButPreserveUris($original) + ); + } + } + /** * @covers Whoops\Util\TemplateHelper::breakOnDelimiter */