mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-23 08:12:43 +00:00
347 lines
11 KiB
PHP
347 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Twig\Tests;
|
|
|
|
use Twig\Environment;
|
|
use Twig\Loader\LoaderInterface;
|
|
|
|
/**
|
|
* This class is adapted from code coming from Zend Framework.
|
|
*
|
|
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
|
|
* @license https://framework.zend.com/license/new-bsd New BSD License
|
|
*/
|
|
class escapingTest extends \PHPUnit\Framework\TestCase
|
|
{
|
|
/**
|
|
* All character encodings supported by htmlspecialchars().
|
|
*/
|
|
protected $htmlSpecialChars = [
|
|
'\'' => ''',
|
|
'"' => '"',
|
|
'<' => '<',
|
|
'>' => '>',
|
|
'&' => '&',
|
|
];
|
|
|
|
protected $htmlAttrSpecialChars = [
|
|
'\'' => ''',
|
|
/* Characters beyond ASCII value 255 to unicode escape */
|
|
'Ā' => 'Ā',
|
|
'😀' => '😀',
|
|
/* Immune chars excluded */
|
|
',' => ',',
|
|
'.' => '.',
|
|
'-' => '-',
|
|
'_' => '_',
|
|
/* Basic alnums excluded */
|
|
'a' => 'a',
|
|
'A' => 'A',
|
|
'z' => 'z',
|
|
'Z' => 'Z',
|
|
'0' => '0',
|
|
'9' => '9',
|
|
/* Basic control characters and null */
|
|
"\r" => '
',
|
|
"\n" => '
',
|
|
"\t" => '	',
|
|
"\0" => '�', // should use Unicode replacement char
|
|
/* Encode chars as named entities where possible */
|
|
'<' => '<',
|
|
'>' => '>',
|
|
'&' => '&',
|
|
'"' => '"',
|
|
/* Encode spaces for quoteless attribute protection */
|
|
' ' => ' ',
|
|
];
|
|
|
|
protected $jsSpecialChars = [
|
|
/* HTML special chars - escape without exception to hex */
|
|
'<' => '\\u003C',
|
|
'>' => '\\u003E',
|
|
'\'' => '\\u0027',
|
|
'"' => '\\u0022',
|
|
'&' => '\\u0026',
|
|
'/' => '\\/',
|
|
/* Characters beyond ASCII value 255 to unicode escape */
|
|
'Ā' => '\\u0100',
|
|
'😀' => '\\uD83D\\uDE00',
|
|
/* Immune chars excluded */
|
|
',' => ',',
|
|
'.' => '.',
|
|
'_' => '_',
|
|
/* Basic alnums excluded */
|
|
'a' => 'a',
|
|
'A' => 'A',
|
|
'z' => 'z',
|
|
'Z' => 'Z',
|
|
'0' => '0',
|
|
'9' => '9',
|
|
/* Basic control characters and null */
|
|
"\r" => '\r',
|
|
"\n" => '\n',
|
|
"\x08" => '\b',
|
|
"\t" => '\t',
|
|
"\x0C" => '\f',
|
|
"\0" => '\\u0000',
|
|
/* Encode spaces for quoteless attribute protection */
|
|
' ' => '\\u0020',
|
|
];
|
|
|
|
protected $urlSpecialChars = [
|
|
/* HTML special chars - escape without exception to percent encoding */
|
|
'<' => '%3C',
|
|
'>' => '%3E',
|
|
'\'' => '%27',
|
|
'"' => '%22',
|
|
'&' => '%26',
|
|
/* Characters beyond ASCII value 255 to hex sequence */
|
|
'Ā' => '%C4%80',
|
|
/* Punctuation and unreserved check */
|
|
',' => '%2C',
|
|
'.' => '.',
|
|
'_' => '_',
|
|
'-' => '-',
|
|
':' => '%3A',
|
|
';' => '%3B',
|
|
'!' => '%21',
|
|
/* Basic alnums excluded */
|
|
'a' => 'a',
|
|
'A' => 'A',
|
|
'z' => 'z',
|
|
'Z' => 'Z',
|
|
'0' => '0',
|
|
'9' => '9',
|
|
/* Basic control characters and null */
|
|
"\r" => '%0D',
|
|
"\n" => '%0A',
|
|
"\t" => '%09',
|
|
"\0" => '%00',
|
|
/* PHP quirks from the past */
|
|
' ' => '%20',
|
|
'~' => '~',
|
|
'+' => '%2B',
|
|
];
|
|
|
|
protected $cssSpecialChars = [
|
|
/* HTML special chars - escape without exception to hex */
|
|
'<' => '\\3C ',
|
|
'>' => '\\3E ',
|
|
'\'' => '\\27 ',
|
|
'"' => '\\22 ',
|
|
'&' => '\\26 ',
|
|
/* Characters beyond ASCII value 255 to unicode escape */
|
|
'Ā' => '\\100 ',
|
|
/* Immune chars excluded */
|
|
',' => '\\2C ',
|
|
'.' => '\\2E ',
|
|
'_' => '\\5F ',
|
|
/* Basic alnums excluded */
|
|
'a' => 'a',
|
|
'A' => 'A',
|
|
'z' => 'z',
|
|
'Z' => 'Z',
|
|
'0' => '0',
|
|
'9' => '9',
|
|
/* Basic control characters and null */
|
|
"\r" => '\\D ',
|
|
"\n" => '\\A ',
|
|
"\t" => '\\9 ',
|
|
"\0" => '\\0 ',
|
|
/* Encode spaces for quoteless attribute protection */
|
|
' ' => '\\20 ',
|
|
];
|
|
|
|
protected $env;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->env = new Environment($this->createMock(LoaderInterface::class));
|
|
}
|
|
|
|
public function testHtmlEscapingConvertsSpecialChars()
|
|
{
|
|
foreach ($this->htmlSpecialChars as $key => $value) {
|
|
$this->assertEquals($value, twig_escape_filter($this->env, $key, 'html'), 'Failed to escape: '.$key);
|
|
}
|
|
}
|
|
|
|
public function testHtmlAttributeEscapingConvertsSpecialChars()
|
|
{
|
|
foreach ($this->htmlAttrSpecialChars as $key => $value) {
|
|
$this->assertEquals($value, twig_escape_filter($this->env, $key, 'html_attr'), 'Failed to escape: '.$key);
|
|
}
|
|
}
|
|
|
|
public function testJavascriptEscapingConvertsSpecialChars()
|
|
{
|
|
foreach ($this->jsSpecialChars as $key => $value) {
|
|
$this->assertEquals($value, twig_escape_filter($this->env, $key, 'js'), 'Failed to escape: '.$key);
|
|
}
|
|
}
|
|
|
|
public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding()
|
|
{
|
|
$twig = new Environment($this->createMock(LoaderInterface::class));
|
|
$previousInternalEncoding = mb_internal_encoding();
|
|
try {
|
|
mb_internal_encoding('ISO-8859-1');
|
|
foreach ($this->jsSpecialChars as $key => $value) {
|
|
$this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: ' . $key);
|
|
}
|
|
} finally {
|
|
if ($previousInternalEncoding !== false) {
|
|
mb_internal_encoding($previousInternalEncoding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testJavascriptEscapingReturnsStringIfZeroLength()
|
|
{
|
|
$this->assertEquals('', twig_escape_filter($this->env, '', 'js'));
|
|
}
|
|
|
|
public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits()
|
|
{
|
|
$this->assertEquals('123', twig_escape_filter($this->env, '123', 'js'));
|
|
}
|
|
|
|
public function testCssEscapingConvertsSpecialChars()
|
|
{
|
|
foreach ($this->cssSpecialChars as $key => $value) {
|
|
$this->assertEquals($value, twig_escape_filter($this->env, $key, 'css'), 'Failed to escape: '.$key);
|
|
}
|
|
}
|
|
|
|
public function testCssEscapingReturnsStringIfZeroLength()
|
|
{
|
|
$this->assertEquals('', twig_escape_filter($this->env, '', 'css'));
|
|
}
|
|
|
|
public function testCssEscapingReturnsStringIfContainsOnlyDigits()
|
|
{
|
|
$this->assertEquals('123', twig_escape_filter($this->env, '123', 'css'));
|
|
}
|
|
|
|
public function testUrlEscapingConvertsSpecialChars()
|
|
{
|
|
foreach ($this->urlSpecialChars as $key => $value) {
|
|
$this->assertEquals($value, twig_escape_filter($this->env, $key, 'url'), 'Failed to escape: '.$key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Range tests to confirm escaped range of characters is within OWASP recommendation.
|
|
*/
|
|
|
|
/**
|
|
* Only testing the first few 2 ranges on this prot. function as that's all these
|
|
* other range tests require.
|
|
*/
|
|
public function testUnicodeCodepointConversionToUtf8()
|
|
{
|
|
$expected = ' ~ޙ';
|
|
$codepoints = [0x20, 0x7e, 0x799];
|
|
$result = '';
|
|
foreach ($codepoints as $value) {
|
|
$result .= $this->codepointToUtf8($value);
|
|
}
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
/**
|
|
* Convert a Unicode Codepoint to a literal UTF-8 character.
|
|
*
|
|
* @param int $codepoint Unicode codepoint in hex notation
|
|
*
|
|
* @return string UTF-8 literal string
|
|
*/
|
|
protected function codepointToUtf8($codepoint)
|
|
{
|
|
if ($codepoint < 0x80) {
|
|
return \chr($codepoint);
|
|
}
|
|
if ($codepoint < 0x800) {
|
|
return \chr($codepoint >> 6 & 0x3f | 0xc0)
|
|
.\chr($codepoint & 0x3f | 0x80);
|
|
}
|
|
if ($codepoint < 0x10000) {
|
|
return \chr($codepoint >> 12 & 0x0f | 0xe0)
|
|
.\chr($codepoint >> 6 & 0x3f | 0x80)
|
|
.\chr($codepoint & 0x3f | 0x80);
|
|
}
|
|
if ($codepoint < 0x110000) {
|
|
return \chr($codepoint >> 18 & 0x07 | 0xf0)
|
|
.\chr($codepoint >> 12 & 0x3f | 0x80)
|
|
.\chr($codepoint >> 6 & 0x3f | 0x80)
|
|
.\chr($codepoint & 0x3f | 0x80);
|
|
}
|
|
throw new \Exception('Codepoint requested outside of Unicode range.');
|
|
}
|
|
|
|
public function testJavascriptEscapingEscapesOwaspRecommendedRanges()
|
|
{
|
|
$immune = [',', '.', '_']; // Exceptions to escaping ranges
|
|
for ($chr = 0; $chr < 0xFF; ++$chr) {
|
|
if ($chr >= 0x30 && $chr <= 0x39
|
|
|| $chr >= 0x41 && $chr <= 0x5A
|
|
|| $chr >= 0x61 && $chr <= 0x7A) {
|
|
$literal = $this->codepointToUtf8($chr);
|
|
$this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js'));
|
|
} else {
|
|
$literal = $this->codepointToUtf8($chr);
|
|
if (\in_array($literal, $immune)) {
|
|
$this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js'));
|
|
} else {
|
|
$this->assertNotEquals(
|
|
$literal,
|
|
twig_escape_filter($this->env, $literal, 'js'),
|
|
"$literal should be escaped!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges()
|
|
{
|
|
$immune = [',', '.', '-', '_']; // Exceptions to escaping ranges
|
|
for ($chr = 0; $chr < 0xFF; ++$chr) {
|
|
if ($chr >= 0x30 && $chr <= 0x39
|
|
|| $chr >= 0x41 && $chr <= 0x5A
|
|
|| $chr >= 0x61 && $chr <= 0x7A) {
|
|
$literal = $this->codepointToUtf8($chr);
|
|
$this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr'));
|
|
} else {
|
|
$literal = $this->codepointToUtf8($chr);
|
|
if (\in_array($literal, $immune)) {
|
|
$this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr'));
|
|
} else {
|
|
$this->assertNotEquals(
|
|
$literal,
|
|
twig_escape_filter($this->env, $literal, 'html_attr'),
|
|
"$literal should be escaped!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testCssEscapingEscapesOwaspRecommendedRanges()
|
|
{
|
|
// CSS has no exceptions to escaping ranges
|
|
for ($chr = 0; $chr < 0xFF; ++$chr) {
|
|
if ($chr >= 0x30 && $chr <= 0x39
|
|
|| $chr >= 0x41 && $chr <= 0x5A
|
|
|| $chr >= 0x61 && $chr <= 0x7A) {
|
|
$literal = $this->codepointToUtf8($chr);
|
|
$this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'css'));
|
|
} else {
|
|
$literal = $this->codepointToUtf8($chr);
|
|
$this->assertNotEquals(
|
|
$literal,
|
|
twig_escape_filter($this->env, $literal, 'css'),
|
|
"$literal should be escaped!");
|
|
}
|
|
}
|
|
}
|
|
}
|