escapingTest.php 11KB

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