Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 13 additions & 19 deletions Slim/Error/Renderers/HtmlErrorRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use Throwable;

use function get_class;
use function htmlentities;
use function htmlspecialchars;
use function sprintf;

Expand All @@ -33,11 +32,7 @@ public function __invoke(Throwable $exception, bool $displayErrorDetails): strin
$html .= '<h2>Details</h2>';
$html .= $this->renderExceptionFragment($exception);
} else {
$description = htmlspecialchars(
$this->getErrorDescription($exception),
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
$description = $this->escapeHtml($this->getErrorDescription($exception));
$html = "<p>{$description}</p>";
}

Expand All @@ -46,26 +41,25 @@ public function __invoke(Throwable $exception, bool $displayErrorDetails): strin

private function renderExceptionFragment(Throwable $exception): string
{
$html = sprintf('<div><strong>Type:</strong> %s</div>', get_class($exception));

$code = $exception->getCode();
$html .= sprintf('<div><strong>Code:</strong> %s</div>', $code);

$html .= sprintf('<div><strong>Message:</strong> %s</div>', htmlentities($exception->getMessage()));

$html .= sprintf('<div><strong>File:</strong> %s</div>', $exception->getFile());

$html .= sprintf('<div><strong>Line:</strong> %s</div>', $exception->getLine());

$html = sprintf('<div><strong>Type:</strong> %s</div>', $this->escapeHtml(get_class($exception)));
$html .= sprintf('<div><strong>Code:</strong> %s</div>', $this->escapeHtml((string) $exception->getCode()));
$html .= sprintf('<div><strong>Message:</strong> %s</div>', $this->escapeHtml($exception->getMessage()));
$html .= sprintf('<div><strong>File:</strong> %s</div>', $this->escapeHtml($exception->getFile()));
$html .= sprintf('<div><strong>Line:</strong> %s</div>', $this->escapeHtml((string) $exception->getLine()));
$html .= '<h2>Trace</h2>';
$html .= sprintf('<pre>%s</pre>', htmlentities($exception->getTraceAsString()));
$html .= sprintf('<pre>%s</pre>', $this->escapeHtml($exception->getTraceAsString()));

return $html;
}

private function escapeHtml(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

public function renderHtmlBody(string $title = '', string $html = ''): string
{
$title = htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$title = $this->escapeHtml($title);

return sprintf(
'<!doctype html>' .
Expand Down
74 changes: 74 additions & 0 deletions tests/Error/AbstractErrorRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@
use Slim\Tests\TestCase;
use stdClass;

use function bin2hex;
use function file_put_contents;
use function get_class;
use function htmlspecialchars;
use function json_decode;
use function json_encode;
use function mkdir;
use function random_bytes;
use function rmdir;
use function simplexml_load_string;
use function sys_get_temp_dir;
use function unlink;

use const ENT_QUOTES;
use const ENT_SUBSTITUTE;
Expand Down Expand Up @@ -74,6 +82,72 @@ public function testHTMLErrorRendererRenderFragmentMethod()
$this->assertMatchesRegularExpression('/.*Line*/', $output);
}

public function testHTMLErrorRendererEscapesQuotesInErrorDetails()
{
$unsafe = "O'Brien <script>";
$escaped = htmlspecialchars($unsafe, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

$exception = new class ($unsafe) extends Exception {
public function __construct(string $unsafe)
{
parent::__construct($unsafe, 0);
$this->file = '/tmp/' . $unsafe . '.php';
$this->code = "SQL'" . $unsafe;
$this->line = 7;
}
};

$renderer = new HtmlErrorRenderer();
$reflectionRenderer = new ReflectionClass(HtmlErrorRenderer::class);

$method = $reflectionRenderer->getMethod('renderExceptionFragment');
$this->setAccessible($method);
$output = $method->invoke($renderer, $exception);

$this->assertStringNotContainsString($unsafe, $output);
$this->assertStringContainsString($escaped, $output);
$this->assertStringContainsString(
htmlspecialchars('/tmp/' . $unsafe . '.php', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
$output
);
$this->assertStringContainsString(
htmlspecialchars("SQL'" . $unsafe, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
$output
);
$this->assertStringContainsString('<div><strong>Line:</strong> 7</div>', $output);
}

public function testHTMLErrorRendererEscapesAnonymousClassType()
{
$unsafe = "O'Brien <script>";
$dir = sys_get_temp_dir() . '/slim-html-' . bin2hex(random_bytes(4));
$unsafeDir = $dir . '/' . $unsafe;
mkdir($unsafeDir, 0700, true);
$file = $unsafeDir . '/anon.php';
file_put_contents($file, '<?php return new class extends Exception {};');

try {
/** @var Exception $exception */
$exception = require $file;
$renderer = new HtmlErrorRenderer();
$reflectionRenderer = new ReflectionClass(HtmlErrorRenderer::class);

$method = $reflectionRenderer->getMethod('renderExceptionFragment');
$this->setAccessible($method);
$output = $method->invoke($renderer, $exception);

$this->assertStringNotContainsString($unsafe, $output);
$this->assertStringContainsString(
htmlspecialchars(get_class($exception), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
$output
);
} finally {
unlink($file);
rmdir($unsafeDir);
rmdir($dir);
}
}

public function testHTMLErrorRendererRenderHttpException()
{
$exceptionTitle = 'title';
Expand Down
Loading