Mercurial > hg > isophonics-drupal-site
comparison core/tests/Drupal/Tests/Component/Utility/XssTest.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 129ea1e6d783 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Tests\Component\Utility; | |
4 | |
5 use Drupal\Component\Utility\Html; | |
6 use Drupal\Component\Utility\UrlHelper; | |
7 use Drupal\Component\Utility\Xss; | |
8 use PHPUnit\Framework\TestCase; | |
9 | |
10 /** | |
11 * XSS Filtering tests. | |
12 * | |
13 * @group Utility | |
14 * | |
15 * @coversDefaultClass \Drupal\Component\Utility\Xss | |
16 * | |
17 * Script injection vectors mostly adopted from http://ha.ckers.org/xss.html. | |
18 * | |
19 * Relevant CVEs: | |
20 * - CVE-2002-1806, ~CVE-2005-0682, ~CVE-2005-2106, CVE-2005-3973, | |
21 * CVE-2006-1226 (= rev. 1.112?), CVE-2008-0273, CVE-2008-3740. | |
22 */ | |
23 class XssTest extends TestCase { | |
24 | |
25 /** | |
26 * {@inheritdoc} | |
27 */ | |
28 protected function setUp() { | |
29 parent::setUp(); | |
30 | |
31 $allowed_protocols = [ | |
32 'http', | |
33 'https', | |
34 'ftp', | |
35 'news', | |
36 'nntp', | |
37 'telnet', | |
38 'mailto', | |
39 'irc', | |
40 'ssh', | |
41 'sftp', | |
42 'webcal', | |
43 'rtsp', | |
44 ]; | |
45 UrlHelper::setAllowedProtocols($allowed_protocols); | |
46 } | |
47 | |
48 /** | |
49 * Tests limiting allowed tags and XSS prevention. | |
50 * | |
51 * XSS tests assume that script is disallowed by default and src is allowed | |
52 * by default, but on* and style attributes are disallowed. | |
53 * | |
54 * @param string $value | |
55 * The value to filter. | |
56 * @param string $expected | |
57 * The expected result. | |
58 * @param string $message | |
59 * The assertion message to display upon failure. | |
60 * @param array $allowed_tags | |
61 * (optional) The allowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter(). | |
62 * | |
63 * @dataProvider providerTestFilterXssNormalized | |
64 */ | |
65 public function testFilterXssNormalized($value, $expected, $message, array $allowed_tags = NULL) { | |
66 if ($allowed_tags === NULL) { | |
67 $value = Xss::filter($value); | |
68 } | |
69 else { | |
70 $value = Xss::filter($value, $allowed_tags); | |
71 } | |
72 $this->assertNormalized($value, $expected, $message); | |
73 } | |
74 | |
75 /** | |
76 * Data provider for testFilterXssNormalized(). | |
77 * | |
78 * @see testFilterXssNormalized() | |
79 * | |
80 * @return array | |
81 * An array of arrays containing strings: | |
82 * - The value to filter. | |
83 * - The value to expect after filtering. | |
84 * - The assertion message. | |
85 * - (optional) The allowed HTML HTML tags array that should be passed to | |
86 * \Drupal\Component\Utility\Xss::filter(). | |
87 */ | |
88 public function providerTestFilterXssNormalized() { | |
89 return [ | |
90 [ | |
91 "Who's Online", | |
92 "who's online", | |
93 'HTML filter -- html entity number', | |
94 ], | |
95 [ | |
96 "Who&#039;s Online", | |
97 "who's online", | |
98 'HTML filter -- encoded html entity number', | |
99 ], | |
100 [ | |
101 "Who&amp;#039; Online", | |
102 "who&#039; online", | |
103 'HTML filter -- double encoded html entity number', | |
104 ], | |
105 // Custom elements with dashes in the tag name. | |
106 [ | |
107 "<test-element></test-element>", | |
108 "<test-element></test-element>", | |
109 'Custom element with dashes in tag name.', | |
110 ['test-element'], | |
111 ], | |
112 ]; | |
113 } | |
114 | |
115 /** | |
116 * Tests limiting to allowed tags and XSS prevention. | |
117 * | |
118 * XSS tests assume that script is disallowed by default and src is allowed | |
119 * by default, but on* and style attributes are disallowed. | |
120 * | |
121 * @param string $value | |
122 * The value to filter. | |
123 * @param string $expected | |
124 * The string that is expected to be missing. | |
125 * @param string $message | |
126 * The assertion message to display upon failure. | |
127 * @param array $allowed_tags | |
128 * (optional) The allowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter(). | |
129 * | |
130 * @dataProvider providerTestFilterXssNotNormalized | |
131 */ | |
132 public function testFilterXssNotNormalized($value, $expected, $message, array $allowed_tags = NULL) { | |
133 if ($allowed_tags === NULL) { | |
134 $value = Xss::filter($value); | |
135 } | |
136 else { | |
137 $value = Xss::filter($value, $allowed_tags); | |
138 } | |
139 $this->assertNotNormalized($value, $expected, $message); | |
140 } | |
141 | |
142 /** | |
143 * Data provider for testFilterXssNotNormalized(). | |
144 * | |
145 * @see testFilterXssNotNormalized() | |
146 * | |
147 * @return array | |
148 * An array of arrays containing the following elements: | |
149 * - The value to filter. | |
150 * - The value to expect that's missing after filtering. | |
151 * - The assertion message. | |
152 * - (optional) The allowed HTML HTML tags array that should be passed to | |
153 * \Drupal\Component\Utility\Xss::filter(). | |
154 */ | |
155 public function providerTestFilterXssNotNormalized() { | |
156 $cases = [ | |
157 // Tag stripping, different ways to work around removal of HTML tags. | |
158 [ | |
159 '<script>alert(0)</script>', | |
160 'script', | |
161 'HTML tag stripping -- simple script without special characters.', | |
162 ], | |
163 [ | |
164 '<script src="http://www.example.com" />', | |
165 'script', | |
166 'HTML tag stripping -- empty script with source.', | |
167 ], | |
168 [ | |
169 '<ScRipt sRc=http://www.example.com/>', | |
170 'script', | |
171 'HTML tag stripping evasion -- varying case.', | |
172 ], | |
173 [ | |
174 "<script\nsrc\n=\nhttp://www.example.com/\n>", | |
175 'script', | |
176 'HTML tag stripping evasion -- multiline tag.', | |
177 ], | |
178 [ | |
179 '<script/a src=http://www.example.com/a.js></script>', | |
180 'script', | |
181 'HTML tag stripping evasion -- non whitespace character after tag name.', | |
182 ], | |
183 [ | |
184 '<script/src=http://www.example.com/a.js></script>', | |
185 'script', | |
186 'HTML tag stripping evasion -- no space between tag and attribute.', | |
187 ], | |
188 // Null between < and tag name works at least with IE6. | |
189 [ | |
190 "<\0scr\0ipt>alert(0)</script>", | |
191 'ipt', | |
192 'HTML tag stripping evasion -- breaking HTML with nulls.', | |
193 ], | |
194 [ | |
195 "<scrscriptipt src=http://www.example.com/a.js>", | |
196 'script', | |
197 'HTML tag stripping evasion -- filter just removing "script".', | |
198 ], | |
199 [ | |
200 '<<script>alert(0);//<</script>', | |
201 'script', | |
202 'HTML tag stripping evasion -- double opening brackets.', | |
203 ], | |
204 [ | |
205 '<script src=http://www.example.com/a.js?<b>', | |
206 'script', | |
207 'HTML tag stripping evasion -- no closing tag.', | |
208 ], | |
209 // DRUPAL-SA-2008-047: This doesn't seem exploitable, but the filter should | |
210 // work consistently. | |
211 [ | |
212 '<script>>', | |
213 'script', | |
214 'HTML tag stripping evasion -- double closing tag.', | |
215 ], | |
216 [ | |
217 '<script src=//www.example.com/.a>', | |
218 'script', | |
219 'HTML tag stripping evasion -- no scheme or ending slash.', | |
220 ], | |
221 [ | |
222 '<script src=http://www.example.com/.a', | |
223 'script', | |
224 'HTML tag stripping evasion -- no closing bracket.', | |
225 ], | |
226 [ | |
227 '<script src=http://www.example.com/ <', | |
228 'script', | |
229 'HTML tag stripping evasion -- opening instead of closing bracket.', | |
230 ], | |
231 [ | |
232 '<nosuchtag attribute="newScriptInjectionVector">', | |
233 'nosuchtag', | |
234 'HTML tag stripping evasion -- unknown tag.', | |
235 ], | |
236 [ | |
237 '<t:set attributeName="innerHTML" to="<script defer>alert(0)</script>">', | |
238 't:set', | |
239 'HTML tag stripping evasion -- colon in the tag name (namespaces\' tricks).', | |
240 ], | |
241 [ | |
242 '<img """><script>alert(0)</script>', | |
243 'script', | |
244 'HTML tag stripping evasion -- a malformed image tag.', | |
245 ['img'], | |
246 ], | |
247 [ | |
248 '<blockquote><script>alert(0)</script></blockquote>', | |
249 'script', | |
250 'HTML tag stripping evasion -- script in a blockqoute.', | |
251 ['blockquote'], | |
252 ], | |
253 [ | |
254 "<!--[if true]><script>alert(0)</script><![endif]-->", | |
255 'script', | |
256 'HTML tag stripping evasion -- script within a comment.', | |
257 ], | |
258 // Dangerous attributes removal. | |
259 [ | |
260 '<p onmouseover="http://www.example.com/">', | |
261 'onmouseover', | |
262 'HTML filter attributes removal -- events, no evasion.', | |
263 ['p'], | |
264 ], | |
265 [ | |
266 '<li style="list-style-image: url(javascript:alert(0))">', | |
267 'style', | |
268 'HTML filter attributes removal -- style, no evasion.', | |
269 ['li'], | |
270 ], | |
271 [ | |
272 '<img onerror =alert(0)>', | |
273 'onerror', | |
274 'HTML filter attributes removal evasion -- spaces before equals sign.', | |
275 ['img'], | |
276 ], | |
277 [ | |
278 '<img onabort!#$%&()*~+-_.,:;?@[/|\]^`=alert(0)>', | |
279 'onabort', | |
280 'HTML filter attributes removal evasion -- non alphanumeric characters before equals sign.', | |
281 ['img'], | |
282 ], | |
283 [ | |
284 '<img oNmediAError=alert(0)>', | |
285 'onmediaerror', | |
286 'HTML filter attributes removal evasion -- varying case.', | |
287 ['img'], | |
288 ], | |
289 // Works at least with IE6. | |
290 [ | |
291 "<img o\0nfocus\0=alert(0)>", | |
292 'focus', | |
293 'HTML filter attributes removal evasion -- breaking with nulls.', | |
294 ['img'], | |
295 ], | |
296 // Only whitelisted scheme names allowed in attributes. | |
297 [ | |
298 '<img src="javascript:alert(0)">', | |
299 'javascript', | |
300 'HTML scheme clearing -- no evasion.', | |
301 ['img'], | |
302 ], | |
303 [ | |
304 '<img src=javascript:alert(0)>', | |
305 'javascript', | |
306 'HTML scheme clearing evasion -- no quotes.', | |
307 ['img'], | |
308 ], | |
309 // A bit like CVE-2006-0070. | |
310 [ | |
311 '<img src="javascript:confirm(0)">', | |
312 'javascript', | |
313 'HTML scheme clearing evasion -- no alert ;)', | |
314 ['img'], | |
315 ], | |
316 [ | |
317 '<img src=`javascript:alert(0)`>', | |
318 'javascript', | |
319 'HTML scheme clearing evasion -- grave accents.', | |
320 ['img'], | |
321 ], | |
322 [ | |
323 '<img dynsrc="javascript:alert(0)">', | |
324 'javascript', | |
325 'HTML scheme clearing -- rare attribute.', | |
326 ['img'], | |
327 ], | |
328 [ | |
329 '<table background="javascript:alert(0)">', | |
330 'javascript', | |
331 'HTML scheme clearing -- another tag.', | |
332 ['table'], | |
333 ], | |
334 [ | |
335 '<base href="javascript:alert(0);//">', | |
336 'javascript', | |
337 'HTML scheme clearing -- one more attribute and tag.', | |
338 ['base'], | |
339 ], | |
340 [ | |
341 '<img src="jaVaSCriPt:alert(0)">', | |
342 'javascript', | |
343 'HTML scheme clearing evasion -- varying case.', | |
344 ['img'], | |
345 ], | |
346 [ | |
347 '<img src=javascript:alert(0)>', | |
348 'javascript', | |
349 'HTML scheme clearing evasion -- UTF-8 decimal encoding.', | |
350 ['img'], | |
351 ], | |
352 [ | |
353 '<img src=javascript:alert(0)>', | |
354 'javascript', | |
355 'HTML scheme clearing evasion -- long UTF-8 encoding.', | |
356 ['img'], | |
357 ], | |
358 [ | |
359 '<img src=javascript:alert(0)>', | |
360 'javascript', | |
361 'HTML scheme clearing evasion -- UTF-8 hex encoding.', | |
362 ['img'], | |
363 ], | |
364 [ | |
365 "<img src=\"jav\tascript:alert(0)\">", | |
366 'script', | |
367 'HTML scheme clearing evasion -- an embedded tab.', | |
368 ['img'], | |
369 ], | |
370 [ | |
371 '<img src="jav	ascript:alert(0)">', | |
372 'script', | |
373 'HTML scheme clearing evasion -- an encoded, embedded tab.', | |
374 ['img'], | |
375 ], | |
376 [ | |
377 '<img src="jav
ascript:alert(0)">', | |
378 'script', | |
379 'HTML scheme clearing evasion -- an encoded, embedded newline.', | |
380 ['img'], | |
381 ], | |
382 // With 
 this test would fail, but the entity gets turned into | |
383 // &#xD;, so it's OK. | |
384 [ | |
385 '<img src="jav
ascript:alert(0)">', | |
386 'script', | |
387 'HTML scheme clearing evasion -- an encoded, embedded carriage return.', | |
388 ['img'], | |
389 ], | |
390 [ | |
391 "<img src=\"\n\n\nj\na\nva\ns\ncript:alert(0)\">", | |
392 'cript', | |
393 'HTML scheme clearing evasion -- broken into many lines.', | |
394 ['img'], | |
395 ], | |
396 [ | |
397 "<img src=\"jav\0a\0\0cript:alert(0)\">", | |
398 'cript', | |
399 'HTML scheme clearing evasion -- embedded nulls.', | |
400 ['img'], | |
401 ], | |
402 [ | |
403 '<img src="vbscript:msgbox(0)">', | |
404 'vbscript', | |
405 'HTML scheme clearing evasion -- another scheme.', | |
406 ['img'], | |
407 ], | |
408 [ | |
409 '<img src="nosuchscheme:notice(0)">', | |
410 'nosuchscheme', | |
411 'HTML scheme clearing evasion -- unknown scheme.', | |
412 ['img'], | |
413 ], | |
414 // Netscape 4.x javascript entities. | |
415 [ | |
416 '<br size="&{alert(0)}">', | |
417 'alert', | |
418 'Netscape 4.x javascript entities.', | |
419 ['br'], | |
420 ], | |
421 // DRUPAL-SA-2008-006: Invalid UTF-8, these only work as reflected XSS with | |
422 // Internet Explorer 6. | |
423 [ | |
424 "<p arg=\"\xe0\">\" style=\"background-image: url(javascript:alert(0));\"\xe0<p>", | |
425 'style', | |
426 'HTML filter -- invalid UTF-8.', | |
427 ['p'], | |
428 ], | |
429 ]; | |
430 // @fixme This dataset currently fails under 5.4 because of | |
431 // https://www.drupal.org/node/1210798. Restore after its fixed. | |
432 if (version_compare(PHP_VERSION, '5.4.0', '<')) { | |
433 $cases[] = [ | |
434 '<img src="  javascript:alert(0)">', | |
435 'javascript', | |
436 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.', | |
437 ['img'], | |
438 ]; | |
439 } | |
440 return $cases; | |
441 } | |
442 | |
443 /** | |
444 * Checks that invalid multi-byte sequences are rejected. | |
445 * | |
446 * @param string $value | |
447 * The value to filter. | |
448 * @param string $expected | |
449 * The expected result. | |
450 * @param string $message | |
451 * The assertion message to display upon failure. | |
452 * | |
453 * @dataProvider providerTestInvalidMultiByte | |
454 */ | |
455 public function testInvalidMultiByte($value, $expected, $message) { | |
456 $this->assertEquals(Xss::filter($value), $expected, $message); | |
457 } | |
458 | |
459 /** | |
460 * Data provider for testInvalidMultiByte(). | |
461 * | |
462 * @see testInvalidMultiByte() | |
463 * | |
464 * @return array | |
465 * An array of arrays containing strings: | |
466 * - The value to filter. | |
467 * - The value to expect after filtering. | |
468 * - The assertion message. | |
469 */ | |
470 public function providerTestInvalidMultiByte() { | |
471 return [ | |
472 ["Foo\xC0barbaz", '', 'Xss::filter() accepted invalid sequence "Foo\xC0barbaz"'], | |
473 ["Fooÿñ", "Fooÿñ", 'Xss::filter() rejects valid sequence Fooÿñ"'], | |
474 ["\xc0aaa", '', 'HTML filter -- overlong UTF-8 sequences.'], | |
475 ]; | |
476 } | |
477 | |
478 /** | |
479 * Checks that strings starting with a question sign are correctly processed. | |
480 */ | |
481 public function testQuestionSign() { | |
482 $value = Xss::filter('<?xml:namespace ns="urn:schemas-microsoft-com:time">'); | |
483 $this->assertTrue(stripos($value, '<?xml') === FALSE, 'HTML tag stripping evasion -- starting with a question sign (processing instructions).'); | |
484 } | |
485 | |
486 /** | |
487 * Check that strings in HTML attributes are correctly processed. | |
488 * | |
489 * @covers ::attributes | |
490 * @dataProvider providerTestAttributes | |
491 */ | |
492 public function testAttribute($value, $expected, $message, $allowed_tags = NULL) { | |
493 $value = Xss::filter($value, $allowed_tags); | |
494 $this->assertEquals($expected, $value, $message); | |
495 } | |
496 | |
497 /** | |
498 * Data provider for testFilterXssAdminNotNormalized(). | |
499 */ | |
500 public function providerTestAttributes() { | |
501 return [ | |
502 [ | |
503 '<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">', | |
504 '<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">', | |
505 'Image tag with alt and title attribute', | |
506 ['img'] | |
507 ], | |
508 [ | |
509 '<a href="https://www.drupal.org/" rel="dc:publisher">Drupal</a>', | |
510 '<a href="https://www.drupal.org/" rel="dc:publisher">Drupal</a>', | |
511 'Link tag with rel attribute', | |
512 ['a'] | |
513 ], | |
514 [ | |
515 '<span property="dc:subject">Drupal 8: The best release ever.</span>', | |
516 '<span property="dc:subject">Drupal 8: The best release ever.</span>', | |
517 'Span tag with property attribute', | |
518 ['span'] | |
519 ], | |
520 [ | |
521 '<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">', | |
522 '<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">', | |
523 'Image tag with data attribute', | |
524 ['img'] | |
525 ], | |
526 [ | |
527 '<a data-a2a-url="foo"></a>', | |
528 '<a data-a2a-url="foo"></a>', | |
529 'Link tag with numeric data attribute', | |
530 ['a'] | |
531 ], | |
532 ]; | |
533 } | |
534 | |
535 /** | |
536 * Checks that \Drupal\Component\Utility\Xss::filterAdmin() correctly strips unallowed tags. | |
537 */ | |
538 public function testFilterXSSAdmin() { | |
539 $value = Xss::filterAdmin('<style /><iframe /><frame /><frameset /><meta /><link /><embed /><applet /><param /><layer />'); | |
540 $this->assertEquals($value, '', 'Admin HTML filter -- should never allow some tags.'); | |
541 } | |
542 | |
543 /** | |
544 * Tests the loose, admin HTML filter. | |
545 * | |
546 * @param string $value | |
547 * The value to filter. | |
548 * @param string $expected | |
549 * The expected result. | |
550 * @param string $message | |
551 * The assertion message to display upon failure. | |
552 * | |
553 * @dataProvider providerTestFilterXssAdminNotNormalized | |
554 */ | |
555 public function testFilterXssAdminNotNormalized($value, $expected, $message) { | |
556 $this->assertNotNormalized(Xss::filterAdmin($value), $expected, $message); | |
557 } | |
558 | |
559 /** | |
560 * Data provider for testFilterXssAdminNotNormalized(). | |
561 * | |
562 * @see testFilterXssAdminNotNormalized() | |
563 * | |
564 * @return array | |
565 * An array of arrays containing strings: | |
566 * - The value to filter. | |
567 * - The value to expect after filtering. | |
568 * - The assertion message. | |
569 */ | |
570 public function providerTestFilterXssAdminNotNormalized() { | |
571 return [ | |
572 // DRUPAL-SA-2008-044 | |
573 ['<object />', 'object', 'Admin HTML filter -- should not allow object tag.'], | |
574 ['<script />', 'script', 'Admin HTML filter -- should not allow script tag.'], | |
575 ]; | |
576 } | |
577 | |
578 /** | |
579 * Asserts that a text transformed to lowercase with HTML entities decoded does contain a given string. | |
580 * | |
581 * Otherwise fails the test with a given message, similar to all the | |
582 * SimpleTest assert* functions. | |
583 * | |
584 * Note that this does not remove nulls, new lines and other characters that | |
585 * could be used to obscure a tag or an attribute name. | |
586 * | |
587 * @param string $haystack | |
588 * Text to look in. | |
589 * @param string $needle | |
590 * Lowercase, plain text to look for. | |
591 * @param string $message | |
592 * (optional) Message to display if failed. Defaults to an empty string. | |
593 * @param string $group | |
594 * (optional) The group this message belongs to. Defaults to 'Other'. | |
595 */ | |
596 protected function assertNormalized($haystack, $needle, $message = '', $group = 'Other') { | |
597 $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) !== FALSE, $message, $group); | |
598 } | |
599 | |
600 /** | |
601 * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string. | |
602 * | |
603 * Otherwise fails the test with a given message, similar to all the | |
604 * SimpleTest assert* functions. | |
605 * | |
606 * Note that this does not remove nulls, new lines, and other character that | |
607 * could be used to obscure a tag or an attribute name. | |
608 * | |
609 * @param string $haystack | |
610 * Text to look in. | |
611 * @param string $needle | |
612 * Lowercase, plain text to look for. | |
613 * @param string $message | |
614 * (optional) Message to display if failed. Defaults to an empty string. | |
615 * @param string $group | |
616 * (optional) The group this message belongs to. Defaults to 'Other'. | |
617 */ | |
618 protected function assertNotNormalized($haystack, $needle, $message = '', $group = 'Other') { | |
619 $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group); | |
620 } | |
621 | |
622 } |