e7832a31f0f05079c6e8661e517f935fe80da89b
[yaffs-website] / web / core / tests / Drupal / Tests / Component / Utility / XssTest.php
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 Drupal\Tests\UnitTestCase;
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 UnitTestCase {
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 }