From 7f2cd8f154d8c36ccbd879ce20f8c4f509ac6dde 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 | 7 ++++- tests/Whoops/Util/TemplateHelperTest.php | 38 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Whoops/Util/TemplateHelper.php b/src/Whoops/Util/TemplateHelper.php index 5612c0b..1c6e43e 100644 --- a/src/Whoops/Util/TemplateHelper.php +++ b/src/Whoops/Util/TemplateHelper.php @@ -80,6 +80,11 @@ 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 (e.g. + * javascript:, vbscript:, data:) are left as plain escaped text, + * since linkifying them would allow script execution when the + * generated link is clicked (see GHSA / issue #791). + * * @param string $raw * @return string */ @@ -87,7 +92,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 */