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
52 changes: 31 additions & 21 deletions system/Helpers/html_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ function video($src, string $unsupportedMessage = '', string $attributes = '', a
$video = '<video';

if (_has_protocol($src)) {
$video .= ' src="' . $src . '"';
$video .= ' src="' . _esc_attr($src) . '"';
} elseif ($indexPage) {
$video .= ' src="' . site_url($src) . '"';
$video .= ' src="' . _esc_attr(site_url($src)) . '"';
} else {
$video .= ' src="' . slash_item('baseURL') . $src . '"';
$video .= ' src="' . _esc_attr(slash_item('baseURL') . $src) . '"';
}

if ($attributes !== '') {
Expand All @@ -317,7 +317,7 @@ function video($src, string $unsupportedMessage = '', string $attributes = '', a

if ($unsupportedMessage !== '') {
$video .= _space_indent()
. $unsupportedMessage
. esc($unsupportedMessage, 'html')
. "\n";
}

Expand Down Expand Up @@ -346,11 +346,11 @@ function audio($src, string $unsupportedMessage = '', string $attributes = '', a
$audio = '<audio';

if (_has_protocol($src)) {
$audio .= ' src="' . $src . '"';
$audio .= ' src="' . _esc_attr($src) . '"';
} elseif ($indexPage) {
$audio .= ' src="' . site_url($src) . '"';
$audio .= ' src="' . _esc_attr(site_url($src)) . '"';
} else {
$audio .= ' src="' . slash_item('baseURL') . $src . '"';
$audio .= ' src="' . _esc_attr(slash_item('baseURL') . $src) . '"';
}

if ($attributes !== '') {
Expand All @@ -364,7 +364,7 @@ function audio($src, string $unsupportedMessage = '', string $attributes = '', a
}

if ($unsupportedMessage !== '') {
$audio .= "\n" . _space_indent() . $unsupportedMessage . "\n";
$audio .= "\n" . _space_indent() . esc($unsupportedMessage, 'html') . "\n";
}

return $audio . "</audio>\n";
Expand Down Expand Up @@ -400,7 +400,7 @@ function _media(string $name, array $types = [], string $unsupportedMessage = ''
}

if ($unsupportedMessage !== '') {
$media .= _space_indent() . $unsupportedMessage . "\n";
$media .= _space_indent() . esc($unsupportedMessage, 'html') . "\n";
}

return $media . ('</' . $name . ">\n");
Expand All @@ -425,8 +425,8 @@ function source(string $src, string $type = 'unknown', string $attributes = '',
$src = $indexPage ? site_url($src) : slash_item('baseURL') . $src;
}

$source = '<source src="' . $src
. '" type="' . $type . '"';
$source = '<source src="' . _esc_attr($src)
. '" type="' . _esc_attr($type) . '"';

if ($attributes !== '') {
$source .= ' ' . $attributes;
Expand All @@ -450,10 +450,10 @@ function source(string $src, string $type = 'unknown', string $attributes = '',
*/
function track(string $src, string $kind, string $srcLanguage, string $label): string
{
return '<track src="' . $src
. '" kind="' . $kind
. '" srclang="' . $srcLanguage
. '" label="' . $label
return '<track src="' . _esc_attr($src)
. '" kind="' . _esc_attr($kind)
. '" srclang="' . _esc_attr($srcLanguage)
. '" label="' . _esc_attr($label)
. '"' . _solidus() . '>';
}
}
Expand All @@ -478,7 +478,7 @@ function object(string $data, string $type = 'unknown', string $attributes = '',
$data = $indexPage ? site_url($data) : slash_item('baseURL') . $data;
}

$object = '<object data="' . $data . '" '
$object = '<object data="' . _esc_attr($data) . '" '
. $attributes . '>';

if ($params !== []) {
Expand Down Expand Up @@ -507,9 +507,9 @@ function object(string $data, string $type = 'unknown', string $attributes = '',
*/
function param(string $name, string $value, string $type = 'ref', string $attributes = ''): string
{
return '<param name="' . $name
. '" type="' . $type
. '" value="' . $value
return '<param name="' . _esc_attr($name)
. '" type="' . _esc_attr($type)
. '" value="' . _esc_attr($value)
. '" ' . $attributes . _solidus() . '>';
}
}
Expand All @@ -531,8 +531,8 @@ function embed(string $src, string $type = 'unknown', string $attributes = '', b
$src = $indexPage ? site_url($src) : slash_item('baseURL') . $src;
}

return '<embed src="' . $src
. '" type="' . $type . '" '
return '<embed src="' . _esc_attr($src)
. '" type="' . _esc_attr($type) . '" '
. $attributes . _solidus() . ">\n";
}
}
Expand All @@ -556,3 +556,13 @@ function _space_indent(int $depth = 2): string
return str_repeat(' ', $depth);
}
}

if (! function_exists('_esc_attr')) {
/**
* Escapes a value for use inside a double-quoted HTML attribute.
*/
function _esc_attr(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
137 changes: 137 additions & 0 deletions tests/system/Helpers/HTMLHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -890,4 +890,141 @@ public function testObjectIndexed(): void

$this->assertSame($expected, $object);
}

public function testVideoEscaping(): void
{
$src = 'http://example.com/test.mp4" onerror="alert(1)';
$message = '<script>alert(1)</script>';

$html = video($src, $message, 'controls');

$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringNotContainsString('<script>', $html);
$this->assertStringContainsString('src="' . _esc_attr($src) . '"', $html);
$this->assertStringContainsString('&lt;script&gt;alert(1)&lt;/script&gt;', $html);

$relative = video('test.mp4" onerror="alert(1)');
$this->assertStringNotContainsString('onerror="alert(1)', $relative);

$withArray = video([source('test.mp4', 'video/mp4')], '<script>alert(1)</script>');
$this->assertStringNotContainsString('<script>', $withArray);
$this->assertStringContainsString('&lt;script&gt;alert(1)&lt;/script&gt;', $withArray);
}

public function testAudioEscaping(): void
{
$src = 'http://example.com/test.mp3" onerror="alert(1)';
$message = '<script>alert(1)</script>';

$html = audio($src, $message, 'controls');

$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringNotContainsString('<script>', $html);
$this->assertStringContainsString('src="' . _esc_attr($src) . '"', $html);
$this->assertStringContainsString('&lt;script&gt;alert(1)&lt;/script&gt;', $html);

$relative = audio('test.mp3" onerror="alert(1)');
$this->assertStringNotContainsString('onerror="alert(1)', $relative);
}

public function testMediaEscaping(): void
{
$sources = [
source('http://example.com/test.mp4" onerror="alert(1)', 'video/mp4" onload="alert(1)'),
];
$message = '<script>alert(1)</script>';

$html = _media('video', $sources, $message);

$this->assertStringNotContainsString('<script>', $html);
$this->assertStringContainsString('&lt;script&gt;alert(1)&lt;/script&gt;', $html);
$this->assertStringNotContainsString('onerror="alert(1)', $html);
}

public function testSourceEscaping(): void
{
$src = 'http://example.com/test.mp4" onerror="alert(1)';
$type = 'video/mp4" onload="alert(1)';

$html = source($src, $type);

$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringNotContainsString('onload="alert(1)', $html);
$this->assertStringContainsString('src="' . _esc_attr($src) . '"', $html);
$this->assertStringContainsString('type="' . _esc_attr($type) . '"', $html);
$this->assertSame('<source src="http://example.com/test.mp4&quot; onerror=&quot;alert(1)" type="video/mp4&quot; onload=&quot;alert(1)">', $html);
}

public function testTrackEscaping(): void
{
$src = 'subtitles.vtt" onerror="alert(1)';
$kind = 'subtitles" onfocus="alert(1)';
$srcLanguage = 'en" onclick="alert(1)';
$label = 'English" onmouseover="alert(1)';

$html = track($src, $kind, $srcLanguage, $label);

$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringNotContainsString('onfocus="alert(1)', $html);
$this->assertStringNotContainsString('onclick="alert(1)', $html);
$this->assertStringNotContainsString('onmouseover="alert(1)', $html);
$this->assertStringContainsString('src="' . _esc_attr($src) . '"', $html);
$this->assertStringContainsString('kind="' . _esc_attr($kind) . '"', $html);
$this->assertStringContainsString('srclang="' . _esc_attr($srcLanguage) . '"', $html);
$this->assertStringContainsString('label="' . _esc_attr($label) . '"', $html);
}

public function testObjectEscaping(): void
{
$data = 'http://example.com/movie.swf" onerror="alert(1)';

$html = object($data);

$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringContainsString('data="' . _esc_attr($data) . '"', $html);
}

public function testParamEscaping(): void
{
$name = 'foo" onclick="alert(1)';
$value = 'bar" onerror="alert(1)';
$type = 'ref" onfocus="alert(1)';

$html = param($name, $value, $type);

$this->assertStringNotContainsString('onclick="alert(1)', $html);
$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringNotContainsString('onfocus="alert(1)', $html);
$this->assertStringContainsString('name="' . _esc_attr($name) . '"', $html);
$this->assertStringContainsString('value="' . _esc_attr($value) . '"', $html);
$this->assertStringContainsString('type="' . _esc_attr($type) . '"', $html);
}

public function testEmbedEscaping(): void
{
$src = 'http://example.com/movie.mov" onerror="alert(1)';
$type = 'video/quicktime" onload="alert(1)';

$html = embed($src, $type);

$this->assertStringNotContainsString('onerror="alert(1)', $html);
$this->assertStringNotContainsString('onload="alert(1)', $html);
$this->assertStringContainsString('src="' . _esc_attr($src) . '"', $html);
$this->assertStringContainsString('type="' . _esc_attr($type) . '"', $html);
}

public function testEscAttrFunctionLiteralOutput(): void
{
$this->assertSame(
'http://example.com/test.mp4?a=1&amp;b=2',
_esc_attr('http://example.com/test.mp4?a=1&b=2'),
);
$this->assertSame(
'foo&quot;bar&#039;&lt;baz&gt;',
_esc_attr('foo"bar\'<baz>'),
);

// ENT_SUBSTITUTE replaces invalid UTF-8 instead of returning an empty string.
$this->assertSame("\u{FFFD}", _esc_attr("\xFF"));
}
}
Loading