EscaperTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Test\Unit;
  7. use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
  8. use Magento\Framework\Escaper;
  9. /**
  10. * \Magento\Framework\Escaper test case
  11. */
  12. class EscaperTest extends \PHPUnit\Framework\TestCase
  13. {
  14. /**
  15. * @var \Magento\Framework\Escaper
  16. */
  17. protected $escaper = null;
  18. /**
  19. * @var \Magento\Framework\ZendEscaper
  20. */
  21. private $zendEscaper;
  22. /**
  23. * @var \Psr\Log\LoggerInterface
  24. */
  25. private $loggerMock;
  26. protected function setUp()
  27. {
  28. $this->escaper = new Escaper();
  29. $this->zendEscaper = new \Magento\Framework\ZendEscaper();
  30. $this->loggerMock = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class);
  31. $objectManagerHelper = new ObjectManager($this);
  32. $objectManagerHelper->setBackwardCompatibleProperty($this->escaper, 'escaper', $this->zendEscaper);
  33. $objectManagerHelper->setBackwardCompatibleProperty($this->escaper, 'logger', $this->loggerMock);
  34. }
  35. /**
  36. * Convert a unicode codepoint to a literal UTF-8 character
  37. *
  38. * @param int $codepoint Unicode codepoint in hex notation
  39. * @return string UTF-8 literal string
  40. */
  41. protected function codepointToUtf8($codepoint)
  42. {
  43. if ($codepoint < 0x80) {
  44. return chr($codepoint);
  45. }
  46. if ($codepoint < 0x800) {
  47. return chr($codepoint >> 6 & 0x3f | 0xc0)
  48. . chr($codepoint & 0x3f | 0x80);
  49. }
  50. if ($codepoint < 0x10000) {
  51. return chr($codepoint >> 12 & 0x0f | 0xe0)
  52. . chr($codepoint >> 6 & 0x3f | 0x80)
  53. . chr($codepoint & 0x3f | 0x80);
  54. }
  55. if ($codepoint < 0x110000) {
  56. return chr($codepoint >> 18 & 0x07 | 0xf0)
  57. . chr($codepoint >> 12 & 0x3f | 0x80)
  58. . chr($codepoint >> 6 & 0x3f | 0x80)
  59. . chr($codepoint & 0x3f | 0x80);
  60. }
  61. throw new \Exception('Codepoint requested outside of unicode range');
  62. }
  63. public function testEscapeJsEscapesOwaspRecommendedRanges()
  64. {
  65. // Exceptions to escaping ranges
  66. $immune = [',', '.', '_'];
  67. for ($chr = 0; $chr < 0xFF; $chr++) {
  68. if (($chr >= 0x30 && $chr <= 0x39)
  69. || ($chr >= 0x41 && $chr <= 0x5A)
  70. || ($chr >= 0x61 && $chr <= 0x7A)
  71. ) {
  72. $literal = $this->codepointToUtf8($chr);
  73. $this->assertEquals($literal, $this->escaper->escapeJs($literal));
  74. } else {
  75. $literal = $this->codepointToUtf8($chr);
  76. if (in_array($literal, $immune)) {
  77. $this->assertEquals($literal, $this->escaper->escapeJs($literal));
  78. } else {
  79. $this->assertNotEquals(
  80. $literal,
  81. $this->escaper->escapeJs($literal),
  82. $literal . ' should be escaped!'
  83. );
  84. }
  85. }
  86. }
  87. }
  88. /**
  89. * @param string $data
  90. * @param string $expected
  91. * @dataProvider escapeJsDataProvider
  92. */
  93. public function testEscapeJs($data, $expected)
  94. {
  95. $this->assertEquals($expected, $this->escaper->escapeJs($data));
  96. }
  97. /**
  98. * @return array
  99. */
  100. public function escapeJsDataProvider()
  101. {
  102. return [
  103. 'zero length string' => ['', ''],
  104. 'only digits' => ['123', '123'],
  105. '<' => ['<', '\u003C'],
  106. '>' => ['>', '\\u003E'],
  107. '\'' => ['\'', '\\u0027'],
  108. '"' => ['"', '\\u0022'],
  109. '&' => ['&', '\\u0026'],
  110. 'Characters beyond ASCII value 255 to unicode escape' => ['Ā', '\\u0100'],
  111. 'Characters beyond Unicode BMP to unicode escape' => ["\xF0\x90\x80\x80", '\\uD800DC00'],
  112. /* Immune chars excluded */
  113. ',' => [',', ','],
  114. '.' => ['.', '.'],
  115. '_' => ['_', '_'],
  116. /* Basic alnums exluded */
  117. 'a' => ['a', 'a'],
  118. 'A' => ['A', 'A'],
  119. 'z' => ['z', 'z'],
  120. 'Z' => ['Z', 'Z'],
  121. '0' => ['0', '0'],
  122. '9' => ['9', '9'],
  123. /* Basic control characters and null */
  124. "\r" => ["\r", '\\u000D'],
  125. "\n" => ["\n", '\\u000A'],
  126. "\t" => ["\t", '\\u0009'],
  127. "\0" => ["\0", '\\u0000'],
  128. 'Encode spaces for quoteless attribute protection' => [' ', '\\u0020'],
  129. ];
  130. }
  131. /**
  132. * @covers \Magento\Framework\Escaper::escapeHtml
  133. * @dataProvider escapeHtmlDataProvider
  134. */
  135. public function testEscapeHtml($data, $expected, $allowedTags = [])
  136. {
  137. $actual = $this->escaper->escapeHtml($data, $allowedTags);
  138. $this->assertEquals($expected, $actual);
  139. }
  140. /**
  141. * @covers \Magento\Framework\Escaper::escapeHtml
  142. * @dataProvider escapeHtmlInvalidDataProvider
  143. */
  144. public function testEscapeHtmlWithInvalidData($data, $expected, $allowedTags = [])
  145. {
  146. $this->loggerMock->expects($this->once())
  147. ->method('critical');
  148. $actual = $this->escaper->escapeHtml($data, $allowedTags);
  149. $this->assertEquals($expected, $actual);
  150. }
  151. /**
  152. * @return array
  153. */
  154. public function escapeHtmlDataProvider()
  155. {
  156. return [
  157. 'array -> [text with no tags, text with no allowed tags]' => [
  158. 'data' => ['one', '<two>three</two>'],
  159. 'expected' => ['one', '&lt;two&gt;three&lt;/two&gt;'],
  160. ],
  161. 'text with special characters' => [
  162. 'data' => '&<>"\'&amp;&lt;&gt;&quot;&#039;&#9;',
  163. 'expected' => '&amp;&lt;&gt;&quot;&#039;&amp;&lt;&gt;&quot;&#039;&#9;'
  164. ],
  165. 'text with special characters and allowed tag' => [
  166. 'data' => '&<br/>"\'&amp;&lt;&gt;&quot;&#039;&#9;',
  167. 'expected' => '&amp;<br>&quot;&#039;&amp;&lt;&gt;&quot;&#039;&#9;',
  168. 'allowedTags' => ['br'],
  169. ],
  170. 'text with multiple allowed tags, includes self closing tag' => [
  171. 'data' => '<span>some text in tags<br /></span>',
  172. 'expected' => '<span>some text in tags<br></span>',
  173. 'allowedTags' => ['span', 'br'],
  174. ],
  175. 'text with multiple allowed tags and allowed attribute in double quotes' => [
  176. 'data' => 'Only <span id="sku_max_allowed"><b>2</b></span> in stock',
  177. 'expected' => 'Only <span id="sku_max_allowed"><b>2</b></span> in stock',
  178. 'allowedTags' => ['span', 'b'],
  179. ],
  180. 'text with multiple allowed tags and allowed attribute in single quotes' => [
  181. 'data' => 'Only <span id=\'sku_max_allowed\'><b>2</b></span> in stock',
  182. 'expected' => 'Only <span id="sku_max_allowed"><b>2</b></span> in stock',
  183. 'allowedTags' => ['span', 'b'],
  184. ],
  185. 'text with multiple allowed tags with allowed attribute' => [
  186. 'data' => 'Only registered users can write reviews. Please <a href="%1">Sign in</a> or <a href="%2">'
  187. . 'create an account</a>',
  188. 'expected' => 'Only registered users can write reviews. Please <a href="%1">Sign in</a> or '
  189. . '<a href="%2">create an account</a>',
  190. 'allowedTags' => ['a'],
  191. ],
  192. 'text with not allowed attribute in single quotes' => [
  193. 'data' => 'Only <span type=\'1\'><b>2</b></span> in stock',
  194. 'expected' => 'Only <span><b>2</b></span> in stock',
  195. 'allowedTags' => ['span', 'b'],
  196. ],
  197. 'text with allowed and not allowed tags' => [
  198. 'data' => 'Only registered users can write reviews. Please <a href="%1">Sign in<span>three</span></a> '
  199. . 'or <a href="%2"><span id="action">create an account</span></a>',
  200. 'expected' => 'Only registered users can write reviews. Please <a href="%1">Sign inthree</a> or '
  201. . '<a href="%2">create an account</a>',
  202. 'allowedTags' => ['a'],
  203. ],
  204. 'text with allowed and not allowed tags, with allowed and not allowed attributes' => [
  205. 'data' => 'Some test <span>text in span tag</span> <strong>text in strong tag</strong> '
  206. . '<a type="some-type" href="http://domain.com/" onclick="alert(1)">Click here</a><script>alert(1)'
  207. . '</script>',
  208. 'expected' => 'Some test <span>text in span tag</span> text in strong tag <a href="http://domain.com/">'
  209. . 'Click here</a>alert(1)',
  210. 'allowedTags' => ['a', 'span'],
  211. ],
  212. 'text with html comment' => [
  213. 'data' => 'Only <span><b>2</b></span> in stock <!-- HTML COMMENT -->',
  214. 'expected' => 'Only <span><b>2</b></span> in stock <!-- HTML COMMENT -->',
  215. 'allowedTags' => ['span', 'b'],
  216. ],
  217. 'text with non ascii characters' => [
  218. 'data' => ['абвгд', 'مثال', '幸福'],
  219. 'expected' => ['абвгд', 'مثال', '幸福'],
  220. 'allowedTags' => [],
  221. ],
  222. 'html and body tags' => [
  223. 'data' => '<html><body><span>String</span></body></html>',
  224. 'expected' => '<span>String</span>',
  225. 'allowedTags' => ['span'],
  226. ],
  227. 'invalid tag' => [
  228. 'data' => '<some tag> some text',
  229. 'expected' => ' some text',
  230. 'allowedTags' => ['span'],
  231. ],
  232. ];
  233. }
  234. /**
  235. * @return array
  236. */
  237. public function escapeHtmlInvalidDataProvider()
  238. {
  239. return [
  240. 'text with allowed script tag' => [
  241. 'data' => '<span><script>some text in tags</script></span>',
  242. 'expected' => '<span>some text in tags</span>',
  243. 'allowedTags' => ['span', 'script'],
  244. ],
  245. 'text with invalid html' => [
  246. 'data' => '<spa>n id="id1">Some string</span>',
  247. 'expected' => 'n id=&quot;id1&quot;&gt;Some string',
  248. 'allowedTags' => ['span'],
  249. ],
  250. ];
  251. }
  252. /**
  253. * @covers \Magento\Framework\Escaper::escapeUrl
  254. */
  255. public function testEscapeUrl()
  256. {
  257. $data = 'http://example.com/search?term=this+%26+that&view=list';
  258. $expected = 'http://example.com/search?term=this+%26+that&amp;view=list';
  259. $this->assertEquals($expected, $this->escaper->escapeUrl($data));
  260. $this->assertEquals($expected, $this->escaper->escapeUrl($expected));
  261. }
  262. /**
  263. * @covers \Magento\Framework\Escaper::escapeJsQuote
  264. */
  265. public function testEscapeJsQuote()
  266. {
  267. $data = ["Don't do that.", 'lost_key' => "Can't do that."];
  268. $expected = ["Don\\'t do that.", "Can\\'t do that."];
  269. $this->assertEquals($expected, $this->escaper->escapeJsQuote($data));
  270. $this->assertEquals($expected[0], $this->escaper->escapeJsQuote($data[0]));
  271. }
  272. /**
  273. * @covers \Magento\Framework\Escaper::escapeQuote
  274. */
  275. public function testEscapeQuote()
  276. {
  277. $data = "Text with 'single' and \"double\" quotes";
  278. $expected = [
  279. "Text with &#039;single&#039; and &quot;double&quot; quotes",
  280. "Text with \\&#039;single\\&#039; and \\&quot;double\\&quot; quotes",
  281. ];
  282. $this->assertEquals($expected[0], $this->escaper->escapeQuote($data));
  283. $this->assertEquals($expected[1], $this->escaper->escapeQuote($data, true));
  284. }
  285. /**
  286. * @covers \Magento\Framework\Escaper::escapeXssInUrl
  287. * @param string $input
  288. * @param string $expected
  289. * @dataProvider escapeDataProvider
  290. */
  291. public function testEscapeXssInUrl($input, $expected)
  292. {
  293. $this->assertEquals($expected, $this->escaper->escapeXssInUrl($input));
  294. }
  295. /**
  296. * Get escape variations
  297. * @return array
  298. */
  299. public function escapeDataProvider()
  300. {
  301. return [
  302. [
  303. 'javascript%3Aalert%28String.fromCharCode%280x78%29%2BString.'
  304. . 'fromCharCode%280x73%29%2BString.fromCharCode%280x73%29%29',
  305. ':alert%28String.fromCharCode%280x78%29%2BString.'
  306. . 'fromCharCode%280x73%29%2BString.fromCharCode%280x73%29%29'
  307. ],
  308. [
  309. 'http://test.com/?redirect=JAVASCRIPT:alert%281%29',
  310. 'http://test.com/?redirect=:alert%281%29',
  311. ],
  312. [
  313. 'http://test.com/?redirect=javascript:alert%281%29',
  314. 'http://test.com/?redirect=:alert%281%29',
  315. ],
  316. [
  317. 'http://test.com/?redirect=JavaScript:alert%281%29',
  318. 'http://test.com/?redirect=:alert%281%29',
  319. ],
  320. [
  321. 'http://test.com/?redirect=javascript:alert(1)',
  322. 'http://test.com/?redirect=:alert(1)',
  323. ],
  324. [
  325. 'http://test.com/?redirect=javascript:alert(1)&test=1',
  326. 'http://test.com/?redirect=:alert(1)&amp;test=1',
  327. ],
  328. [
  329. 'http://test.com/?redirect=\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74:alert(1)',
  330. 'http://test.com/?redirect=:alert(1)',
  331. ],
  332. [
  333. 'http://test.com/?redirect=vbscript:alert(1)',
  334. 'http://test.com/?redirect=:alert(1)',
  335. ],
  336. [
  337. 'http://test.com/?redirect=data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg',
  338. 'http://test.com/?redirect=:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg',
  339. ],
  340. [
  341. 'http://test.com/?redirect=data%3Atext%2Fhtml%3Bbase64%2CPHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg',
  342. 'http://test.com/?redirect=:text%2Fhtml%3Bbase64%2CPHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg',
  343. ],
  344. [
  345. 'http://test.com/?redirect=\x64\x61\x74\x61\x3a\x74\x65\x78\x74x2cCPHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg',
  346. 'http://test.com/?redirect=:\x74\x65\x78\x74x2cCPHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg',
  347. ],
  348. ];
  349. }
  350. }