1<?php 2 3/** 4 * This class is adapted from code coming from Zend Framework. 5 * 6 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) 7 * @license https://framework.zend.com/license/new-bsd New BSD License 8 */ 9class Twig_Test_EscapingTest extends \PHPUnit\Framework\TestCase 10{ 11 /** 12 * All character encodings supported by htmlspecialchars(). 13 */ 14 protected $htmlSpecialChars = [ 15 '\'' => ''', 16 '"' => '"', 17 '<' => '<', 18 '>' => '>', 19 '&' => '&', 20 ]; 21 22 protected $htmlAttrSpecialChars = [ 23 '\'' => ''', 24 /* Characters beyond ASCII value 255 to unicode escape */ 25 'Ā' => 'Ā', 26 '' => '😀', 27 /* Immune chars excluded */ 28 ',' => ',', 29 '.' => '.', 30 '-' => '-', 31 '_' => '_', 32 /* Basic alnums excluded */ 33 'a' => 'a', 34 'A' => 'A', 35 'z' => 'z', 36 'Z' => 'Z', 37 '0' => '0', 38 '9' => '9', 39 /* Basic control characters and null */ 40 "\r" => '
', 41 "\n" => '
', 42 "\t" => '	', 43 "\0" => '�', // should use Unicode replacement char 44 /* Encode chars as named entities where possible */ 45 '<' => '<', 46 '>' => '>', 47 '&' => '&', 48 '"' => '"', 49 /* Encode spaces for quoteless attribute protection */ 50 ' ' => ' ', 51 ]; 52 53 protected $jsSpecialChars = [ 54 /* HTML special chars - escape without exception to hex */ 55 '<' => '\\u003C', 56 '>' => '\\u003E', 57 '\'' => '\\u0027', 58 '"' => '\\u0022', 59 '&' => '\\u0026', 60 '/' => '\\/', 61 /* Characters beyond ASCII value 255 to unicode escape */ 62 'Ā' => '\\u0100', 63 '' => '\\uD83D\\uDE00', 64 /* Immune chars excluded */ 65 ',' => ',', 66 '.' => '.', 67 '_' => '_', 68 /* Basic alnums excluded */ 69 'a' => 'a', 70 'A' => 'A', 71 'z' => 'z', 72 'Z' => 'Z', 73 '0' => '0', 74 '9' => '9', 75 /* Basic control characters and null */ 76 "\r" => '\r', 77 "\n" => '\n', 78 "\x08" => '\b', 79 "\t" => '\t', 80 "\x0C" => '\f', 81 "\0" => '\\u0000', 82 /* Encode spaces for quoteless attribute protection */ 83 ' ' => '\\u0020', 84 ]; 85 86 protected $urlSpecialChars = [ 87 /* HTML special chars - escape without exception to percent encoding */ 88 '<' => '%3C', 89 '>' => '%3E', 90 '\'' => '%27', 91 '"' => '%22', 92 '&' => '%26', 93 /* Characters beyond ASCII value 255 to hex sequence */ 94 'Ā' => '%C4%80', 95 /* Punctuation and unreserved check */ 96 ',' => '%2C', 97 '.' => '.', 98 '_' => '_', 99 '-' => '-', 100 ':' => '%3A', 101 ';' => '%3B', 102 '!' => '%21', 103 /* Basic alnums excluded */ 104 'a' => 'a', 105 'A' => 'A', 106 'z' => 'z', 107 'Z' => 'Z', 108 '0' => '0', 109 '9' => '9', 110 /* Basic control characters and null */ 111 "\r" => '%0D', 112 "\n" => '%0A', 113 "\t" => '%09', 114 "\0" => '%00', 115 /* PHP quirks from the past */ 116 ' ' => '%20', 117 '~' => '~', 118 '+' => '%2B', 119 ]; 120 121 protected $cssSpecialChars = [ 122 /* HTML special chars - escape without exception to hex */ 123 '<' => '\\3C ', 124 '>' => '\\3E ', 125 '\'' => '\\27 ', 126 '"' => '\\22 ', 127 '&' => '\\26 ', 128 /* Characters beyond ASCII value 255 to unicode escape */ 129 'Ā' => '\\100 ', 130 /* Immune chars excluded */ 131 ',' => '\\2C ', 132 '.' => '\\2E ', 133 '_' => '\\5F ', 134 /* Basic alnums excluded */ 135 'a' => 'a', 136 'A' => 'A', 137 'z' => 'z', 138 'Z' => 'Z', 139 '0' => '0', 140 '9' => '9', 141 /* Basic control characters and null */ 142 "\r" => '\\D ', 143 "\n" => '\\A ', 144 "\t" => '\\9 ', 145 "\0" => '\\0 ', 146 /* Encode spaces for quoteless attribute protection */ 147 ' ' => '\\20 ', 148 ]; 149 150 protected $env; 151 152 protected function setUp() 153 { 154 $this->env = new \Twig\Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()); 155 } 156 157 public function testHtmlEscapingConvertsSpecialChars() 158 { 159 foreach ($this->htmlSpecialChars as $key => $value) { 160 $this->assertEquals($value, twig_escape_filter($this->env, $key, 'html'), 'Failed to escape: '.$key); 161 } 162 } 163 164 public function testHtmlAttributeEscapingConvertsSpecialChars() 165 { 166 foreach ($this->htmlAttrSpecialChars as $key => $value) { 167 $this->assertEquals($value, twig_escape_filter($this->env, $key, 'html_attr'), 'Failed to escape: '.$key); 168 } 169 } 170 171 public function testJavascriptEscapingConvertsSpecialChars() 172 { 173 foreach ($this->jsSpecialChars as $key => $value) { 174 $this->assertEquals($value, twig_escape_filter($this->env, $key, 'js'), 'Failed to escape: '.$key); 175 } 176 } 177 178 public function testJavascriptEscapingReturnsStringIfZeroLength() 179 { 180 $this->assertEquals('', twig_escape_filter($this->env, '', 'js')); 181 } 182 183 public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits() 184 { 185 $this->assertEquals('123', twig_escape_filter($this->env, '123', 'js')); 186 } 187 188 public function testCssEscapingConvertsSpecialChars() 189 { 190 foreach ($this->cssSpecialChars as $key => $value) { 191 $this->assertEquals($value, twig_escape_filter($this->env, $key, 'css'), 'Failed to escape: '.$key); 192 } 193 } 194 195 public function testCssEscapingReturnsStringIfZeroLength() 196 { 197 $this->assertEquals('', twig_escape_filter($this->env, '', 'css')); 198 } 199 200 public function testCssEscapingReturnsStringIfContainsOnlyDigits() 201 { 202 $this->assertEquals('123', twig_escape_filter($this->env, '123', 'css')); 203 } 204 205 public function testUrlEscapingConvertsSpecialChars() 206 { 207 foreach ($this->urlSpecialChars as $key => $value) { 208 $this->assertEquals($value, twig_escape_filter($this->env, $key, 'url'), 'Failed to escape: '.$key); 209 } 210 } 211 212 /** 213 * Range tests to confirm escaped range of characters is within OWASP recommendation. 214 */ 215 216 /** 217 * Only testing the first few 2 ranges on this prot. function as that's all these 218 * other range tests require. 219 */ 220 public function testUnicodeCodepointConversionToUtf8() 221 { 222 $expected = ' ~ޙ'; 223 $codepoints = [0x20, 0x7e, 0x799]; 224 $result = ''; 225 foreach ($codepoints as $value) { 226 $result .= $this->codepointToUtf8($value); 227 } 228 $this->assertEquals($expected, $result); 229 } 230 231 /** 232 * Convert a Unicode Codepoint to a literal UTF-8 character. 233 * 234 * @param int $codepoint Unicode codepoint in hex notation 235 * 236 * @return string UTF-8 literal string 237 */ 238 protected function codepointToUtf8($codepoint) 239 { 240 if ($codepoint < 0x80) { 241 return \chr($codepoint); 242 } 243 if ($codepoint < 0x800) { 244 return \chr($codepoint >> 6 & 0x3f | 0xc0) 245 .\chr($codepoint & 0x3f | 0x80); 246 } 247 if ($codepoint < 0x10000) { 248 return \chr($codepoint >> 12 & 0x0f | 0xe0) 249 .\chr($codepoint >> 6 & 0x3f | 0x80) 250 .\chr($codepoint & 0x3f | 0x80); 251 } 252 if ($codepoint < 0x110000) { 253 return \chr($codepoint >> 18 & 0x07 | 0xf0) 254 .\chr($codepoint >> 12 & 0x3f | 0x80) 255 .\chr($codepoint >> 6 & 0x3f | 0x80) 256 .\chr($codepoint & 0x3f | 0x80); 257 } 258 throw new \Exception('Codepoint requested outside of Unicode range.'); 259 } 260 261 public function testJavascriptEscapingEscapesOwaspRecommendedRanges() 262 { 263 $immune = [',', '.', '_']; // Exceptions to escaping ranges 264 for ($chr = 0; $chr < 0xFF; ++$chr) { 265 if ($chr >= 0x30 && $chr <= 0x39 266 || $chr >= 0x41 && $chr <= 0x5A 267 || $chr >= 0x61 && $chr <= 0x7A) { 268 $literal = $this->codepointToUtf8($chr); 269 $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js')); 270 } else { 271 $literal = $this->codepointToUtf8($chr); 272 if (\in_array($literal, $immune)) { 273 $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js')); 274 } else { 275 $this->assertNotEquals( 276 $literal, 277 twig_escape_filter($this->env, $literal, 'js'), 278 "$literal should be escaped!"); 279 } 280 } 281 } 282 } 283 284 public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges() 285 { 286 $immune = [',', '.', '-', '_']; // Exceptions to escaping ranges 287 for ($chr = 0; $chr < 0xFF; ++$chr) { 288 if ($chr >= 0x30 && $chr <= 0x39 289 || $chr >= 0x41 && $chr <= 0x5A 290 || $chr >= 0x61 && $chr <= 0x7A) { 291 $literal = $this->codepointToUtf8($chr); 292 $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr')); 293 } else { 294 $literal = $this->codepointToUtf8($chr); 295 if (\in_array($literal, $immune)) { 296 $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr')); 297 } else { 298 $this->assertNotEquals( 299 $literal, 300 twig_escape_filter($this->env, $literal, 'html_attr'), 301 "$literal should be escaped!"); 302 } 303 } 304 } 305 } 306 307 public function testCssEscapingEscapesOwaspRecommendedRanges() 308 { 309 // CSS has no exceptions to escaping ranges 310 for ($chr = 0; $chr < 0xFF; ++$chr) { 311 if ($chr >= 0x30 && $chr <= 0x39 312 || $chr >= 0x41 && $chr <= 0x5A 313 || $chr >= 0x61 && $chr <= 0x7A) { 314 $literal = $this->codepointToUtf8($chr); 315 $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'css')); 316 } else { 317 $literal = $this->codepointToUtf8($chr); 318 $this->assertNotEquals( 319 $literal, 320 twig_escape_filter($this->env, $literal, 'css'), 321 "$literal should be escaped!"); 322 } 323 } 324 } 325} 326