3 namespace Drupal\Tests\Component\Utility;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\UrlHelper;
7 use Drupal\Component\Utility\Xss;
8 use PHPUnit\Framework\TestCase;
11 * XSS Filtering tests.
15 * @coversDefaultClass \Drupal\Component\Utility\Xss
17 * Script injection vectors mostly adopted from http://ha.ckers.org/xss.html.
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.
23 class XssTest extends TestCase {
28 protected function setUp() {
31 $allowed_protocols = [
45 UrlHelper::setAllowedProtocols($allowed_protocols);
49 * Tests limiting allowed tags and XSS prevention.
51 * XSS tests assume that script is disallowed by default and src is allowed
52 * by default, but on* and style attributes are disallowed.
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().
63 * @dataProvider providerTestFilterXssNormalized
65 public function testFilterXssNormalized($value, $expected, $message, array $allowed_tags = NULL) {
66 if ($allowed_tags === NULL) {
67 $value = Xss::filter($value);
70 $value = Xss::filter($value, $allowed_tags);
72 $this->assertNormalized($value, $expected, $message);
76 * Data provider for testFilterXssNormalized().
78 * @see testFilterXssNormalized()
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().
88 public function providerTestFilterXssNormalized() {
93 'HTML filter -- html entity number',
96 "Who's Online",
98 'HTML filter -- encoded html entity number',
101 "Who' Online",
102 "who' online",
103 'HTML filter -- double encoded html entity number',
105 // Custom elements with dashes in the tag name.
107 "<test-element></test-element>",
108 "<test-element></test-element>",
109 'Custom element with dashes in tag name.',
116 * Tests limiting to allowed tags and XSS prevention.
118 * XSS tests assume that script is disallowed by default and src is allowed
119 * by default, but on* and style attributes are disallowed.
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().
130 * @dataProvider providerTestFilterXssNotNormalized
132 public function testFilterXssNotNormalized($value, $expected, $message, array $allowed_tags = NULL) {
133 if ($allowed_tags === NULL) {
134 $value = Xss::filter($value);
137 $value = Xss::filter($value, $allowed_tags);
139 $this->assertNotNormalized($value, $expected, $message);
143 * Data provider for testFilterXssNotNormalized().
145 * @see testFilterXssNotNormalized()
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().
155 public function providerTestFilterXssNotNormalized() {
157 // Tag stripping, different ways to work around removal of HTML tags.
159 '<script>alert(0)</script>',
161 'HTML tag stripping -- simple script without special characters.',
164 '<script src="http://www.example.com" />',
166 'HTML tag stripping -- empty script with source.',
169 '<ScRipt sRc=http://www.example.com/>',
171 'HTML tag stripping evasion -- varying case.',
174 "<script\nsrc\n=\nhttp://www.example.com/\n>",
176 'HTML tag stripping evasion -- multiline tag.',
179 '<script/a src=http://www.example.com/a.js></script>',
181 'HTML tag stripping evasion -- non whitespace character after tag name.',
184 '<script/src=http://www.example.com/a.js></script>',
186 'HTML tag stripping evasion -- no space between tag and attribute.',
188 // Null between < and tag name works at least with IE6.
190 "<\0scr\0ipt>alert(0)</script>",
192 'HTML tag stripping evasion -- breaking HTML with nulls.',
195 "<scrscriptipt src=http://www.example.com/a.js>",
197 'HTML tag stripping evasion -- filter just removing "script".',
200 '<<script>alert(0);//<</script>',
202 'HTML tag stripping evasion -- double opening brackets.',
205 '<script src=http://www.example.com/a.js?<b>',
207 'HTML tag stripping evasion -- no closing tag.',
209 // DRUPAL-SA-2008-047: This doesn't seem exploitable, but the filter should
210 // work consistently.
214 'HTML tag stripping evasion -- double closing tag.',
217 '<script src=//www.example.com/.a>',
219 'HTML tag stripping evasion -- no scheme or ending slash.',
222 '<script src=http://www.example.com/.a',
224 'HTML tag stripping evasion -- no closing bracket.',
227 '<script src=http://www.example.com/ <',
229 'HTML tag stripping evasion -- opening instead of closing bracket.',
232 '<nosuchtag attribute="newScriptInjectionVector">',
234 'HTML tag stripping evasion -- unknown tag.',
237 '<t:set attributeName="innerHTML" to="<script defer>alert(0)</script>">',
239 'HTML tag stripping evasion -- colon in the tag name (namespaces\' tricks).',
242 '<img """><script>alert(0)</script>',
244 'HTML tag stripping evasion -- a malformed image tag.',
248 '<blockquote><script>alert(0)</script></blockquote>',
250 'HTML tag stripping evasion -- script in a blockqoute.',
254 "<!--[if true]><script>alert(0)</script><![endif]-->",
256 'HTML tag stripping evasion -- script within a comment.',
258 // Dangerous attributes removal.
260 '<p onmouseover="http://www.example.com/">',
262 'HTML filter attributes removal -- events, no evasion.',
266 '<li style="list-style-image: url(javascript:alert(0))">',
268 'HTML filter attributes removal -- style, no evasion.',
272 '<img onerror =alert(0)>',
274 'HTML filter attributes removal evasion -- spaces before equals sign.',
278 '<img onabort!#$%&()*~+-_.,:;?@[/|\]^`=alert(0)>',
280 'HTML filter attributes removal evasion -- non alphanumeric characters before equals sign.',
284 '<img oNmediAError=alert(0)>',
286 'HTML filter attributes removal evasion -- varying case.',
289 // Works at least with IE6.
291 "<img o\0nfocus\0=alert(0)>",
293 'HTML filter attributes removal evasion -- breaking with nulls.',
296 // Only whitelisted scheme names allowed in attributes.
298 '<img src="javascript:alert(0)">',
300 'HTML scheme clearing -- no evasion.',
304 '<img src=javascript:alert(0)>',
306 'HTML scheme clearing evasion -- no quotes.',
309 // A bit like CVE-2006-0070.
311 '<img src="javascript:confirm(0)">',
313 'HTML scheme clearing evasion -- no alert ;)',
317 '<img src=`javascript:alert(0)`>',
319 'HTML scheme clearing evasion -- grave accents.',
323 '<img dynsrc="javascript:alert(0)">',
325 'HTML scheme clearing -- rare attribute.',
329 '<table background="javascript:alert(0)">',
331 'HTML scheme clearing -- another tag.',
335 '<base href="javascript:alert(0);//">',
337 'HTML scheme clearing -- one more attribute and tag.',
341 '<img src="jaVaSCriPt:alert(0)">',
343 'HTML scheme clearing evasion -- varying case.',
347 '<img src=javascript:alert(0)>',
349 'HTML scheme clearing evasion -- UTF-8 decimal encoding.',
353 '<img src=javascript:alert(0)>',
355 'HTML scheme clearing evasion -- long UTF-8 encoding.',
359 '<img src=javascript:alert(0)>',
361 'HTML scheme clearing evasion -- UTF-8 hex encoding.',
365 "<img src=\"jav\tascript:alert(0)\">",
367 'HTML scheme clearing evasion -- an embedded tab.',
371 '<img src="jav	ascript:alert(0)">',
373 'HTML scheme clearing evasion -- an encoded, embedded tab.',
377 '<img src="jav
ascript:alert(0)">',
379 'HTML scheme clearing evasion -- an encoded, embedded newline.',
382 // With 
 this test would fail, but the entity gets turned into
383 // &#xD;, so it's OK.
385 '<img src="jav
ascript:alert(0)">',
387 'HTML scheme clearing evasion -- an encoded, embedded carriage return.',
391 "<img src=\"\n\n\nj\na\nva\ns\ncript:alert(0)\">",
393 'HTML scheme clearing evasion -- broken into many lines.',
397 "<img src=\"jav\0a\0\0cript:alert(0)\">",
399 'HTML scheme clearing evasion -- embedded nulls.',
403 '<img src="vbscript:msgbox(0)">',
405 'HTML scheme clearing evasion -- another scheme.',
409 '<img src="nosuchscheme:notice(0)">',
411 'HTML scheme clearing evasion -- unknown scheme.',
414 // Netscape 4.x javascript entities.
416 '<br size="&{alert(0)}">',
418 'Netscape 4.x javascript entities.',
421 // DRUPAL-SA-2008-006: Invalid UTF-8, these only work as reflected XSS with
422 // Internet Explorer 6.
424 "<p arg=\"\xe0\">\" style=\"background-image: url(javascript:alert(0));\"\xe0<p>",
426 'HTML filter -- invalid UTF-8.',
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', '<')) {
434 '<img src="  javascript:alert(0)">',
436 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.',
444 * Checks that invalid multi-byte sequences are rejected.
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.
453 * @dataProvider providerTestInvalidMultiByte
455 public function testInvalidMultiByte($value, $expected, $message) {
456 $this->assertEquals(Xss::filter($value), $expected, $message);
460 * Data provider for testInvalidMultiByte().
462 * @see testInvalidMultiByte()
465 * An array of arrays containing strings:
466 * - The value to filter.
467 * - The value to expect after filtering.
468 * - The assertion message.
470 public function providerTestInvalidMultiByte() {
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.'],
479 * Checks that strings starting with a question sign are correctly processed.
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).');
487 * Check that strings in HTML attributes are correctly processed.
489 * @covers ::attributes
490 * @dataProvider providerTestAttributes
492 public function testAttribute($value, $expected, $message, $allowed_tags = NULL) {
493 $value = Xss::filter($value, $allowed_tags);
494 $this->assertEquals($expected, $value, $message);
498 * Data provider for testFilterXssAdminNotNormalized().
500 public function providerTestAttributes() {
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',
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',
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',
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',
527 '<a data-a2a-url="foo"></a>',
528 '<a data-a2a-url="foo"></a>',
529 'Link tag with numeric data attribute',
536 * Checks that \Drupal\Component\Utility\Xss::filterAdmin() correctly strips unallowed tags.
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.');
544 * Tests the loose, admin HTML filter.
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.
553 * @dataProvider providerTestFilterXssAdminNotNormalized
555 public function testFilterXssAdminNotNormalized($value, $expected, $message) {
556 $this->assertNotNormalized(Xss::filterAdmin($value), $expected, $message);
560 * Data provider for testFilterXssAdminNotNormalized().
562 * @see testFilterXssAdminNotNormalized()
565 * An array of arrays containing strings:
566 * - The value to filter.
567 * - The value to expect after filtering.
568 * - The assertion message.
570 public function providerTestFilterXssAdminNotNormalized() {
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.'],
579 * Asserts that a text transformed to lowercase with HTML entities decoded does contain a given string.
581 * Otherwise fails the test with a given message, similar to all the
582 * SimpleTest assert* functions.
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.
587 * @param string $haystack
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'.
596 protected function assertNormalized($haystack, $needle, $message = '', $group = 'Other') {
597 $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) !== FALSE, $message, $group);
601 * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string.
603 * Otherwise fails the test with a given message, similar to all the
604 * SimpleTest assert* functions.
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.
609 * @param string $haystack
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'.
618 protected function assertNotNormalized($haystack, $needle, $message = '', $group = 'Other') {
619 $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group);