123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- <?php
- namespace Drupal\Tests\Component\Utility;
- use Drupal\Component\Utility\Html;
- use Drupal\Component\Utility\UrlHelper;
- use Drupal\Component\Utility\Xss;
- use PHPUnit\Framework\TestCase;
- /**
- * XSS Filtering tests.
- *
- * @group Utility
- *
- * @coversDefaultClass \Drupal\Component\Utility\Xss
- *
- * Script injection vectors mostly adopted from http://ha.ckers.org/xss.html.
- *
- * Relevant CVEs:
- * - CVE-2002-1806, ~CVE-2005-0682, ~CVE-2005-2106, CVE-2005-3973,
- * CVE-2006-1226 (= rev. 1.112?), CVE-2008-0273, CVE-2008-3740.
- */
- class XssTest extends TestCase {
- /**
- * {@inheritdoc}
- */
- protected function setUp() {
- parent::setUp();
- $allowed_protocols = [
- 'http',
- 'https',
- 'ftp',
- 'news',
- 'nntp',
- 'telnet',
- 'mailto',
- 'irc',
- 'ssh',
- 'sftp',
- 'webcal',
- 'rtsp',
- ];
- UrlHelper::setAllowedProtocols($allowed_protocols);
- }
- /**
- * Tests limiting allowed tags and XSS prevention.
- *
- * XSS tests assume that script is disallowed by default and src is allowed
- * by default, but on* and style attributes are disallowed.
- *
- * @param string $value
- * The value to filter.
- * @param string $expected
- * The expected result.
- * @param string $message
- * The assertion message to display upon failure.
- * @param array $allowed_tags
- * (optional) The allowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter().
- *
- * @dataProvider providerTestFilterXssNormalized
- */
- public function testFilterXssNormalized($value, $expected, $message, array $allowed_tags = NULL) {
- if ($allowed_tags === NULL) {
- $value = Xss::filter($value);
- }
- else {
- $value = Xss::filter($value, $allowed_tags);
- }
- $this->assertNormalized($value, $expected, $message);
- }
- /**
- * Data provider for testFilterXssNormalized().
- *
- * @see testFilterXssNormalized()
- *
- * @return array
- * An array of arrays containing strings:
- * - The value to filter.
- * - The value to expect after filtering.
- * - The assertion message.
- * - (optional) The allowed HTML HTML tags array that should be passed to
- * \Drupal\Component\Utility\Xss::filter().
- */
- public function providerTestFilterXssNormalized() {
- return [
- [
- "Who's Online",
- "who's online",
- 'HTML filter -- html entity number',
- ],
- [
- "Who&#039;s Online",
- "who's online",
- 'HTML filter -- encoded html entity number',
- ],
- [
- "Who&amp;#039; Online",
- "who&#039; online",
- 'HTML filter -- double encoded html entity number',
- ],
- // Custom elements with dashes in the tag name.
- [
- "<test-element></test-element>",
- "<test-element></test-element>",
- 'Custom element with dashes in tag name.',
- ['test-element'],
- ],
- ];
- }
- /**
- * Tests limiting to allowed tags and XSS prevention.
- *
- * XSS tests assume that script is disallowed by default and src is allowed
- * by default, but on* and style attributes are disallowed.
- *
- * @param string $value
- * The value to filter.
- * @param string $expected
- * The string that is expected to be missing.
- * @param string $message
- * The assertion message to display upon failure.
- * @param array $allowed_tags
- * (optional) The allowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter().
- *
- * @dataProvider providerTestFilterXssNotNormalized
- */
- public function testFilterXssNotNormalized($value, $expected, $message, array $allowed_tags = NULL) {
- if ($allowed_tags === NULL) {
- $value = Xss::filter($value);
- }
- else {
- $value = Xss::filter($value, $allowed_tags);
- }
- $this->assertNotNormalized($value, $expected, $message);
- }
- /**
- * Data provider for testFilterXssNotNormalized().
- *
- * @see testFilterXssNotNormalized()
- *
- * @return array
- * An array of arrays containing the following elements:
- * - The value to filter.
- * - The value to expect that's missing after filtering.
- * - The assertion message.
- * - (optional) The allowed HTML HTML tags array that should be passed to
- * \Drupal\Component\Utility\Xss::filter().
- */
- public function providerTestFilterXssNotNormalized() {
- $cases = [
- // Tag stripping, different ways to work around removal of HTML tags.
- [
- '<script>alert(0)</script>',
- 'script',
- 'HTML tag stripping -- simple script without special characters.',
- ],
- [
- '<script src="http://www.example.com" />',
- 'script',
- 'HTML tag stripping -- empty script with source.',
- ],
- [
- '<ScRipt sRc=http://www.example.com/>',
- 'script',
- 'HTML tag stripping evasion -- varying case.',
- ],
- [
- "<script\nsrc\n=\nhttp://www.example.com/\n>",
- 'script',
- 'HTML tag stripping evasion -- multiline tag.',
- ],
- [
- '<script/a src=http://www.example.com/a.js></script>',
- 'script',
- 'HTML tag stripping evasion -- non whitespace character after tag name.',
- ],
- [
- '<script/src=http://www.example.com/a.js></script>',
- 'script',
- 'HTML tag stripping evasion -- no space between tag and attribute.',
- ],
- // Null between < and tag name works at least with IE6.
- [
- "<\0scr\0ipt>alert(0)</script>",
- 'ipt',
- 'HTML tag stripping evasion -- breaking HTML with nulls.',
- ],
- [
- "<scrscriptipt src=http://www.example.com/a.js>",
- 'script',
- 'HTML tag stripping evasion -- filter just removing "script".',
- ],
- [
- '<<script>alert(0);//<</script>',
- 'script',
- 'HTML tag stripping evasion -- double opening brackets.',
- ],
- [
- '<script src=http://www.example.com/a.js?<b>',
- 'script',
- 'HTML tag stripping evasion -- no closing tag.',
- ],
- // DRUPAL-SA-2008-047: This doesn't seem exploitable, but the filter should
- // work consistently.
- [
- '<script>>',
- 'script',
- 'HTML tag stripping evasion -- double closing tag.',
- ],
- [
- '<script src=//www.example.com/.a>',
- 'script',
- 'HTML tag stripping evasion -- no scheme or ending slash.',
- ],
- [
- '<script src=http://www.example.com/.a',
- 'script',
- 'HTML tag stripping evasion -- no closing bracket.',
- ],
- [
- '<script src=http://www.example.com/ <',
- 'script',
- 'HTML tag stripping evasion -- opening instead of closing bracket.',
- ],
- [
- '<nosuchtag attribute="newScriptInjectionVector">',
- 'nosuchtag',
- 'HTML tag stripping evasion -- unknown tag.',
- ],
- [
- '<t:set attributeName="innerHTML" to="<script defer>alert(0)</script>">',
- 't:set',
- 'HTML tag stripping evasion -- colon in the tag name (namespaces\' tricks).',
- ],
- [
- '<img """><script>alert(0)</script>',
- 'script',
- 'HTML tag stripping evasion -- a malformed image tag.',
- ['img'],
- ],
- [
- '<blockquote><script>alert(0)</script></blockquote>',
- 'script',
- 'HTML tag stripping evasion -- script in a blockqoute.',
- ['blockquote'],
- ],
- [
- "<!--[if true]><script>alert(0)</script><![endif]-->",
- 'script',
- 'HTML tag stripping evasion -- script within a comment.',
- ],
- // Dangerous attributes removal.
- [
- '<p onmouseover="http://www.example.com/">',
- 'onmouseover',
- 'HTML filter attributes removal -- events, no evasion.',
- ['p'],
- ],
- [
- '<li style="list-style-image: url(javascript:alert(0))">',
- 'style',
- 'HTML filter attributes removal -- style, no evasion.',
- ['li'],
- ],
- [
- '<img onerror =alert(0)>',
- 'onerror',
- 'HTML filter attributes removal evasion -- spaces before equals sign.',
- ['img'],
- ],
- [
- '<img onabort!#$%&()*~+-_.,:;?@[/|\]^`=alert(0)>',
- 'onabort',
- 'HTML filter attributes removal evasion -- non alphanumeric characters before equals sign.',
- ['img'],
- ],
- [
- '<img oNmediAError=alert(0)>',
- 'onmediaerror',
- 'HTML filter attributes removal evasion -- varying case.',
- ['img'],
- ],
- // Works at least with IE6.
- [
- "<img o\0nfocus\0=alert(0)>",
- 'focus',
- 'HTML filter attributes removal evasion -- breaking with nulls.',
- ['img'],
- ],
- // Only whitelisted scheme names allowed in attributes.
- [
- '<img src="javascript:alert(0)">',
- 'javascript',
- 'HTML scheme clearing -- no evasion.',
- ['img'],
- ],
- [
- '<img src=javascript:alert(0)>',
- 'javascript',
- 'HTML scheme clearing evasion -- no quotes.',
- ['img'],
- ],
- // A bit like CVE-2006-0070.
- [
- '<img src="javascript:confirm(0)">',
- 'javascript',
- 'HTML scheme clearing evasion -- no alert ;)',
- ['img'],
- ],
- [
- '<img src=`javascript:alert(0)`>',
- 'javascript',
- 'HTML scheme clearing evasion -- grave accents.',
- ['img'],
- ],
- [
- '<img dynsrc="javascript:alert(0)">',
- 'javascript',
- 'HTML scheme clearing -- rare attribute.',
- ['img'],
- ],
- [
- '<table background="javascript:alert(0)">',
- 'javascript',
- 'HTML scheme clearing -- another tag.',
- ['table'],
- ],
- [
- '<base href="javascript:alert(0);//">',
- 'javascript',
- 'HTML scheme clearing -- one more attribute and tag.',
- ['base'],
- ],
- [
- '<img src="jaVaSCriPt:alert(0)">',
- 'javascript',
- 'HTML scheme clearing evasion -- varying case.',
- ['img'],
- ],
- [
- '<img src=javascript:alert(0)>',
- 'javascript',
- 'HTML scheme clearing evasion -- UTF-8 decimal encoding.',
- ['img'],
- ],
- [
- '<img src=javascript:alert(0)>',
- 'javascript',
- 'HTML scheme clearing evasion -- long UTF-8 encoding.',
- ['img'],
- ],
- [
- '<img src=javascript:alert(0)>',
- 'javascript',
- 'HTML scheme clearing evasion -- UTF-8 hex encoding.',
- ['img'],
- ],
- [
- "<img src=\"jav\tascript:alert(0)\">",
- 'script',
- 'HTML scheme clearing evasion -- an embedded tab.',
- ['img'],
- ],
- [
- '<img src="jav	ascript:alert(0)">',
- 'script',
- 'HTML scheme clearing evasion -- an encoded, embedded tab.',
- ['img'],
- ],
- [
- '<img src="jav
ascript:alert(0)">',
- 'script',
- 'HTML scheme clearing evasion -- an encoded, embedded newline.',
- ['img'],
- ],
- // With 
 this test would fail, but the entity gets turned into
- // &#xD;, so it's OK.
- [
- '<img src="jav
ascript:alert(0)">',
- 'script',
- 'HTML scheme clearing evasion -- an encoded, embedded carriage return.',
- ['img'],
- ],
- [
- "<img src=\"\n\n\nj\na\nva\ns\ncript:alert(0)\">",
- 'cript',
- 'HTML scheme clearing evasion -- broken into many lines.',
- ['img'],
- ],
- [
- "<img src=\"jav\0a\0\0cript:alert(0)\">",
- 'cript',
- 'HTML scheme clearing evasion -- embedded nulls.',
- ['img'],
- ],
- [
- '<img src="vbscript:msgbox(0)">',
- 'vbscript',
- 'HTML scheme clearing evasion -- another scheme.',
- ['img'],
- ],
- [
- '<img src="nosuchscheme:notice(0)">',
- 'nosuchscheme',
- 'HTML scheme clearing evasion -- unknown scheme.',
- ['img'],
- ],
- // Netscape 4.x javascript entities.
- [
- '<br size="&{alert(0)}">',
- 'alert',
- 'Netscape 4.x javascript entities.',
- ['br'],
- ],
- // DRUPAL-SA-2008-006: Invalid UTF-8, these only work as reflected XSS with
- // Internet Explorer 6.
- [
- "<p arg=\"\xe0\">\" style=\"background-image: url(javascript:alert(0));\"\xe0<p>",
- 'style',
- 'HTML filter -- invalid UTF-8.',
- ['p'],
- ],
- ];
- // @fixme This dataset currently fails under 5.4 because of
- // https://www.drupal.org/node/1210798. Restore after its fixed.
- if (version_compare(PHP_VERSION, '5.4.0', '<')) {
- $cases[] = [
- '<img src="  javascript:alert(0)">',
- 'javascript',
- 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.',
- ['img'],
- ];
- }
- return $cases;
- }
- /**
- * Checks that invalid multi-byte sequences are rejected.
- *
- * @param string $value
- * The value to filter.
- * @param string $expected
- * The expected result.
- * @param string $message
- * The assertion message to display upon failure.
- *
- * @dataProvider providerTestInvalidMultiByte
- */
- public function testInvalidMultiByte($value, $expected, $message) {
- $this->assertEquals(Xss::filter($value), $expected, $message);
- }
- /**
- * Data provider for testInvalidMultiByte().
- *
- * @see testInvalidMultiByte()
- *
- * @return array
- * An array of arrays containing strings:
- * - The value to filter.
- * - The value to expect after filtering.
- * - The assertion message.
- */
- public function providerTestInvalidMultiByte() {
- return [
- ["Foo\xC0barbaz", '', 'Xss::filter() accepted invalid sequence "Foo\xC0barbaz"'],
- ["Fooÿñ", "Fooÿñ", 'Xss::filter() rejects valid sequence Fooÿñ"'],
- ["\xc0aaa", '', 'HTML filter -- overlong UTF-8 sequences.'],
- ];
- }
- /**
- * Checks that strings starting with a question sign are correctly processed.
- */
- public function testQuestionSign() {
- $value = Xss::filter('<?xml:namespace ns="urn:schemas-microsoft-com:time">');
- $this->assertTrue(stripos($value, '<?xml') === FALSE, 'HTML tag stripping evasion -- starting with a question sign (processing instructions).');
- }
- /**
- * Check that strings in HTML attributes are correctly processed.
- *
- * @covers ::attributes
- * @dataProvider providerTestAttributes
- */
- public function testAttribute($value, $expected, $message, $allowed_tags = NULL) {
- $value = Xss::filter($value, $allowed_tags);
- $this->assertEquals($expected, $value, $message);
- }
- /**
- * Data provider for testFilterXssAdminNotNormalized().
- */
- public function providerTestAttributes() {
- return [
- [
- '<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">',
- '<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">',
- 'Image tag with alt and title attribute',
- ['img'],
- ],
- [
- '<a href="https://www.drupal.org/" rel="dc:publisher">Drupal</a>',
- '<a href="https://www.drupal.org/" rel="dc:publisher">Drupal</a>',
- 'Link tag with rel attribute',
- ['a'],
- ],
- [
- '<span property="dc:subject">Drupal 8: The best release ever.</span>',
- '<span property="dc:subject">Drupal 8: The best release ever.</span>',
- 'Span tag with property attribute',
- ['span'],
- ],
- [
- '<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">',
- '<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">',
- 'Image tag with data attribute',
- ['img'],
- ],
- [
- '<a data-a2a-url="foo"></a>',
- '<a data-a2a-url="foo"></a>',
- 'Link tag with numeric data attribute',
- ['a'],
- ],
- ];
- }
- /**
- * Checks that \Drupal\Component\Utility\Xss::filterAdmin() correctly strips unallowed tags.
- */
- public function testFilterXSSAdmin() {
- $value = Xss::filterAdmin('<style /><iframe /><frame /><frameset /><meta /><link /><embed /><applet /><param /><layer />');
- $this->assertEquals($value, '', 'Admin HTML filter -- should never allow some tags.');
- }
- /**
- * Tests the loose, admin HTML filter.
- *
- * @param string $value
- * The value to filter.
- * @param string $expected
- * The expected result.
- * @param string $message
- * The assertion message to display upon failure.
- *
- * @dataProvider providerTestFilterXssAdminNotNormalized
- */
- public function testFilterXssAdminNotNormalized($value, $expected, $message) {
- $this->assertNotNormalized(Xss::filterAdmin($value), $expected, $message);
- }
- /**
- * Data provider for testFilterXssAdminNotNormalized().
- *
- * @see testFilterXssAdminNotNormalized()
- *
- * @return array
- * An array of arrays containing strings:
- * - The value to filter.
- * - The value to expect after filtering.
- * - The assertion message.
- */
- public function providerTestFilterXssAdminNotNormalized() {
- return [
- // DRUPAL-SA-2008-044
- ['<object />', 'object', 'Admin HTML filter -- should not allow object tag.'],
- ['<script />', 'script', 'Admin HTML filter -- should not allow script tag.'],
- ];
- }
- /**
- * Asserts that a text transformed to lowercase with HTML entities decoded does contain a given string.
- *
- * Otherwise fails the test with a given message, similar to all the
- * SimpleTest assert* functions.
- *
- * Note that this does not remove nulls, new lines and other characters that
- * could be used to obscure a tag or an attribute name.
- *
- * @param string $haystack
- * Text to look in.
- * @param string $needle
- * Lowercase, plain text to look for.
- * @param string $message
- * (optional) Message to display if failed. Defaults to an empty string.
- * @param string $group
- * (optional) The group this message belongs to. Defaults to 'Other'.
- */
- protected function assertNormalized($haystack, $needle, $message = '', $group = 'Other') {
- $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) !== FALSE, $message, $group);
- }
- /**
- * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string.
- *
- * Otherwise fails the test with a given message, similar to all the
- * SimpleTest assert* functions.
- *
- * Note that this does not remove nulls, new lines, and other character that
- * could be used to obscure a tag or an attribute name.
- *
- * @param string $haystack
- * Text to look in.
- * @param string $needle
- * Lowercase, plain text to look for.
- * @param string $message
- * (optional) Message to display if failed. Defaults to an empty string.
- * @param string $group
- * (optional) The group this message belongs to. Defaults to 'Other'.
- */
- protected function assertNotNormalized($haystack, $needle, $message = '', $group = 'Other') {
- $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group);
- }
- }
|