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&#039;s Online",
92 "who's online",
93 'HTML filter -- html entity number',
94 ],
95 [
96 "Who&amp;#039;s Online",
97 "who&#039;s online",
98 'HTML filter -- encoded html entity number',
99 ],
100 [
101 "Who&amp;amp;#039; Online",
102 "who&amp;#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="&lt;script defer&gt;alert(0)&lt;/script&gt;">',
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=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#48;&#41;>',
348 'javascript',
349 'HTML scheme clearing evasion -- UTF-8 decimal encoding.',
350 ['img'],
351 ],
352 [
353 '<img src=&#00000106&#0000097&#00000118&#0000097&#00000115&#0000099&#00000114&#00000105&#00000112&#00000116&#0000058&#0000097&#00000108&#00000101&#00000114&#00000116&#0000040&#0000048&#0000041>',
354 'javascript',
355 'HTML scheme clearing evasion -- long UTF-8 encoding.',
356 ['img'],
357 ],
358 [
359 '<img src=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x30&#x29>',
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&#x09;ascript:alert(0)">',
372 'script',
373 'HTML scheme clearing evasion -- an encoded, embedded tab.',
374 ['img'],
375 ],
376 [
377 '<img src="jav&#x000000A;ascript:alert(0)">',
378 'script',
379 'HTML scheme clearing evasion -- an encoded, embedded newline.',
380 ['img'],
381 ],
382 // With &#xD; this test would fail, but the entity gets turned into
383 // &amp;#xD;, so it's OK.
384 [
385 '<img src="jav&#x0D;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=" &#14; 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 }