XssTest.php 19 KB

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