diff --git a/CHANGELOG b/CHANGELOG index c69e46f10..50cacca52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.27.0 (2026-XX-XX) * Restrict allowed classes in `Twig\Profiler\Profile::unserialize()` to prevent arbitrary class instantiation + * Escape root profile name in `HtmlDumper` * Deprecate the `Twig\Sandbox\SourcePolicyInterface` interface with no replacement # 3.26.0 (2026-05-20) diff --git a/src/Profiler/Dumper/BaseDumper.php b/src/Profiler/Dumper/BaseDumper.php index 267718c1f..06bf4b888 100644 --- a/src/Profiler/Dumper/BaseDumper.php +++ b/src/Profiler/Dumper/BaseDumper.php @@ -31,11 +31,16 @@ abstract class BaseDumper abstract protected function formatTime(Profile $profile, $percent): string; + protected function formatRoot(Profile $profile): string + { + return $profile->getName(); + } + private function dumpProfile(Profile $profile, $prefix = '', $sibling = false): string { if ($profile->isRoot()) { $this->root = $profile->getDuration(); - $start = $profile->getName(); + $start = $this->formatRoot($profile); } else { if ($profile->isTemplate()) { $start = $this->formatTemplate($profile, $prefix); diff --git a/src/Profiler/Dumper/HtmlDumper.php b/src/Profiler/Dumper/HtmlDumper.php index a5e59e498..1bfd81b0e 100644 --- a/src/Profiler/Dumper/HtmlDumper.php +++ b/src/Profiler/Dumper/HtmlDumper.php @@ -30,6 +30,11 @@ final class HtmlDumper extends BaseDumper return '
'.parent::dump($profile).'
'; } + protected function formatRoot(Profile $profile): string + { + return self::escape($profile->getName()); + } + protected function formatTemplate(Profile $profile, $prefix): string { return \sprintf('%s└ %s', $prefix, self::$colors['template'], self::escape($profile->getTemplate())); diff --git a/tests/Profiler/Dumper/HtmlTest.php b/tests/Profiler/Dumper/HtmlTest.php index b1170a83a..75ddb791d 100644 --- a/tests/Profiler/Dumper/HtmlTest.php +++ b/tests/Profiler/Dumper/HtmlTest.php @@ -59,4 +59,14 @@ EOF, $dumper->dump($this->getProfile())); $this->assertStringContainsString('<img src=x onerror=alert(2)>', $output); $this->assertStringContainsString('<img src=x onerror=alert(3)>', $output); } + + public function testDumpEscapesRootProfileName() + { + $root = new Profile('template-name', Profile::ROOT, ''); + + $output = (new HtmlDumper())->dump($root); + + $this->assertStringNotContainsString('', $output); + $this->assertStringContainsString('<img src=x onerror=alert(1)>', $output); + } }