From 1e51c7501483cd483f71df911c1eaf1e629e9933 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 03:00:40 +0200 Subject: [PATCH] fix(template-helper): restrict escapeButPreserveUris to http/https schemes escapeButPreserveUris() matched any scheme via [A-z]+?://, which also turns javascript://, vbscript:// and data:// URIs into clickable links on the generated error page, allowing script execution on click (XSS). The [A-z] class was also invalid, spanning extra ASCII punctuation. Restrict the scheme to http/https, the only two schemes that make sense to linkify on an error page, leaving other schemes as plain escaped text. --- src/Whoops/Util/TemplateHelper.php | 5 +++- tests/Whoops/Util/TemplateHelperTest.php | 38 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Whoops/Util/TemplateHelper.php b/src/Whoops/Util/TemplateHelper.php index 5612c0b..b77b160 100644 --- a/src/Whoops/Util/TemplateHelper.php +++ b/src/Whoops/Util/TemplateHelper.php @@ -80,6 +80,9 @@ 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 linkified; other schemes are left as + * plain escaped text so that a link cannot execute script when clicked. + * * @param string $raw * @return string */ @@ -87,7 +90,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..100b463 100644 --- a/tests/Whoops/Util/TemplateHelperTest.php +++ b/tests/Whoops/Util/TemplateHelperTest.php @@ -63,6 +63,44 @@ class TemplateHelperTest extends TestCase ); } + /** + * @covers Whoops\Util\TemplateHelper::escapeButPreserveUris + */ + public function testEscapeButPreserveUrisRejectsDangerousSchemes() + { + $dangerous = [ + 'javascript://x.co/?%0Aalert(1)', + 'vbscript://x.co/?%0Amsgbox(1)', + 'data://text/html,', + ]; + + foreach ($dangerous as $payload) { + $output = $this->helper->escapeButPreserveUris($payload); + + $this->assertStringNotContainsString( + 'assertEquals( + "http://google.com", + $this->helper->escapeButPreserveUris('http://google.com') + ); + + $this->assertEquals( + "https://google.com", + $this->helper->escapeButPreserveUris('https://google.com') + ); + } + /** * @covers Whoops\Util\TemplateHelper::breakOnDelimiter */