kses.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. /**
  3. * Some simple test cases for KSES post content filtering
  4. *
  5. * @group formatting
  6. * @group kses
  7. */
  8. class Tests_Kses extends WP_UnitTestCase {
  9. /**
  10. * @ticket 20210
  11. */
  12. function test_wp_filter_post_kses_address() {
  13. global $allowedposttags;
  14. $attributes = array(
  15. 'class' => 'classname',
  16. 'id' => 'id',
  17. 'style' => 'color: red;',
  18. 'style' => 'color: red',
  19. 'style' => 'color: red; text-align:center',
  20. 'style' => 'color: red; text-align:center;',
  21. 'title' => 'title',
  22. );
  23. foreach ( $attributes as $name => $value ) {
  24. $string = "<address $name='$value'>1 WordPress Avenue, The Internet.</address>";
  25. $expect_string = "<address $name='" . str_replace( '; ', ';', trim( $value, ';' ) ) . "'>1 WordPress Avenue, The Internet.</address>";
  26. $this->assertEquals( $expect_string, wp_kses( $string, $allowedposttags ) );
  27. }
  28. }
  29. /**
  30. * @ticket 20210
  31. */
  32. function test_wp_filter_post_kses_a() {
  33. global $allowedposttags;
  34. $attributes = array(
  35. 'class' => 'classname',
  36. 'id' => 'id',
  37. 'style' => 'color: red;',
  38. 'title' => 'title',
  39. 'href' => 'http://example.com',
  40. 'rel' => 'related',
  41. 'rev' => 'revision',
  42. 'name' => 'name',
  43. 'target' => '_blank',
  44. );
  45. foreach ( $attributes as $name => $value ) {
  46. $string = "<a $name='$value'>I link this</a>";
  47. $expect_string = "<a $name='" . trim( $value, ';' ) . "'>I link this</a>";
  48. $this->assertEquals( $expect_string, wp_kses( $string, $allowedposttags ) );
  49. }
  50. }
  51. /**
  52. * @ticket 20210
  53. */
  54. function test_wp_filter_post_kses_abbr() {
  55. global $allowedposttags;
  56. $attributes = array(
  57. 'class' => 'classname',
  58. 'id' => 'id',
  59. 'style' => 'color: red;',
  60. 'title' => 'title',
  61. );
  62. foreach ( $attributes as $name => $value ) {
  63. $string = "<abbr $name='$value'>WP</abbr>";
  64. $expect_string = "<abbr $name='" . trim( $value, ';' ) . "'>WP</abbr>";
  65. $this->assertEquals( $expect_string, wp_kses( $string, $allowedposttags ) );
  66. }
  67. }
  68. function test_feed_links() {
  69. global $allowedposttags;
  70. $content = <<<EOF
  71. <a href="feed:javascript:alert(1)">CLICK ME</a>
  72. <a href="feed:javascript:feed:alert(1)">CLICK ME</a>
  73. <a href="feed:feed:javascript:alert(1)">CLICK ME</a>
  74. <a href="javascript:feed:alert(1)">CLICK ME</a>
  75. <a href="javascript:feed:javascript:alert(1)">CLICK ME</a>
  76. <a href="feed:feed:feed:javascript:alert(1)">CLICK ME</a>
  77. <a href="feed:feed:feed:feed:javascript:alert(1)">CLICK ME</a>
  78. <a href="feed:feed:feed:feed:feed:javascript:alert(1)">CLICK ME</a>
  79. <a href="feed:javascript:feed:javascript:feed:javascript:alert(1)">CLICK ME</a>
  80. <a href="feed:javascript:feed:javascript:feed:javascript:feed:javascript:feed:javascript:alert(1)">CLICK ME</a>
  81. <a href="feed:feed:feed:http:alert(1)">CLICK ME</a>
  82. EOF;
  83. $expected = <<<EOF
  84. <a href="feed:alert(1)">CLICK ME</a>
  85. <a href="feed:feed:alert(1)">CLICK ME</a>
  86. <a href="feed:feed:alert(1)">CLICK ME</a>
  87. <a href="feed:alert(1)">CLICK ME</a>
  88. <a href="feed:alert(1)">CLICK ME</a>
  89. <a href="">CLICK ME</a>
  90. <a href="">CLICK ME</a>
  91. <a href="">CLICK ME</a>
  92. <a href="">CLICK ME</a>
  93. <a href="">CLICK ME</a>
  94. <a href="">CLICK ME</a>
  95. EOF;
  96. $this->assertEquals( $expected, wp_kses( $content, $allowedposttags ) );
  97. }
  98. function test_wp_kses_bad_protocol() {
  99. $bad = array(
  100. 'dummy:alert(1)',
  101. 'javascript:alert(1)',
  102. 'JaVaScRiPt:alert(1)',
  103. 'javascript:alert(1);',
  104. 'javascript&#58;alert(1);',
  105. 'javascript&#0058;alert(1);',
  106. 'javascript&#0000058alert(1);',
  107. 'javascript&#x3A;alert(1);',
  108. 'javascript&#X3A;alert(1);',
  109. 'javascript&#X3a;alert(1);',
  110. 'javascript&#x3a;alert(1);',
  111. 'javascript&#x003a;alert(1);',
  112. '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
  113. 'jav ascript:alert(1);',
  114. 'jav&#x09;ascript:alert(1);',
  115. 'jav&#x0A;ascript:alert(1);',
  116. 'jav&#x0D;ascript:alert(1);',
  117. ' &#14; javascript:alert(1);',
  118. 'javascript:javascript:alert(1);',
  119. 'javascript&#58;javascript:alert(1);',
  120. 'javascript&#0000058javascript:alert(1);',
  121. 'javascript:javascript&#58;alert(1);',
  122. 'javascript:javascript&#0000058alert(1);',
  123. 'javascript&#0000058alert(1)//?:',
  124. 'feed:javascript:alert(1)',
  125. 'feed:javascript:feed:javascript:feed:javascript:alert(1)',
  126. );
  127. foreach ( $bad as $k => $x ) {
  128. $result = wp_kses_bad_protocol( wp_kses_normalize_entities( $x ), wp_allowed_protocols() );
  129. if ( ! empty( $result ) && $result != 'alert(1);' && $result != 'alert(1)' ) {
  130. switch ( $k ) {
  131. case 6: $this->assertEquals( 'javascript&amp;#0000058alert(1);', $result ); break;
  132. case 12:
  133. $this->assertEquals( str_replace( '&', '&amp;', $x ), $result );
  134. break;
  135. case 22: $this->assertEquals( 'javascript&amp;#0000058alert(1);', $result ); break;
  136. case 23: $this->assertEquals( 'javascript&amp;#0000058alert(1)//?:', $result ); break;
  137. case 24: $this->assertEquals( 'feed:alert(1)', $result ); break;
  138. default: $this->fail( "wp_kses_bad_protocol failed on $x. Result: $result" );
  139. }
  140. }
  141. }
  142. $safe = array(
  143. 'dummy:alert(1)',
  144. 'HTTP://example.org/',
  145. 'http://example.org/',
  146. 'http&#58;//example.org/',
  147. 'http&#x3A;//example.org/',
  148. 'https://example.org',
  149. 'http://example.org/wp-admin/post.php?post=2&amp;action=edit',
  150. 'http://example.org/index.php?test=&#039;blah&#039;',
  151. );
  152. foreach ( $safe as $x ) {
  153. $result = wp_kses_bad_protocol( wp_kses_normalize_entities( $x ), array( 'http', 'https', 'dummy' ) );
  154. if ( $result != $x && $result != 'http://example.org/' )
  155. $this->fail( "wp_kses_bad_protocol incorrectly blocked $x" );
  156. }
  157. }
  158. public function test_hackers_attacks() {
  159. $xss = simplexml_load_file( DIR_TESTDATA . '/formatting/xssAttacks.xml' );
  160. foreach ( $xss->attack as $attack ) {
  161. if ( in_array( $attack->name, array( 'IMG Embedded commands 2', 'US-ASCII encoding', 'OBJECT w/Flash 2', 'Character Encoding Example' ) ) )
  162. continue;
  163. $code = (string) $attack->code;
  164. if ( $code == 'See Below' )
  165. continue;
  166. if ( substr( $code, 0, 4 ) == 'perl' ) {
  167. $pos = strpos( $code, '"' ) + 1;
  168. $code = substr( $code, $pos, strrpos($code, '"') - $pos );
  169. $code = str_replace( '\0', "\0", $code );
  170. }
  171. $result = trim( wp_kses_data( $code ) );
  172. if ( $result == '' || $result == 'XSS' || $result == 'alert("XSS");' || $result == "alert('XSS');" )
  173. continue;
  174. switch ( $attack->name ) {
  175. case 'XSS Locator':
  176. $this->assertEquals('\';alert(String.fromCharCode(88,83,83))//\\\';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//\\";alert(String.fromCharCode(88,83,83))//--&gt;"&gt;\'&gt;alert(String.fromCharCode(88,83,83))=', $result);
  177. break;
  178. case 'XSS Quick Test':
  179. $this->assertEquals('\'\';!--"=', $result);
  180. break;
  181. case 'SCRIPT w/Alert()':
  182. $this->assertEquals( "alert('XSS')", $result );
  183. break;
  184. case 'SCRIPT w/Char Code':
  185. $this->assertEquals('alert(String.fromCharCode(88,83,83))', $result);
  186. break;
  187. case 'IMG STYLE w/expression':
  188. $this->assertEquals('exp/*', $result);
  189. break;
  190. case 'List-style-image':
  191. $this->assertEquals('li {list-style-image: url("javascript:alert(\'XSS\')");}XSS', $result);
  192. break;
  193. case 'STYLE':
  194. $this->assertEquals( "alert('XSS');", $result);
  195. break;
  196. case 'STYLE w/background-image':
  197. $this->assertEquals('.XSS{background-image:url("javascript:alert(\'XSS\')");}<A></A>', $result);
  198. break;
  199. case 'STYLE w/background':
  200. $this->assertEquals('BODY{background:url("javascript:alert(\'XSS\')")}', $result);
  201. break;
  202. case 'Remote Stylesheet 2':
  203. $this->assertEquals( "@import'http://ha.ckers.org/xss.css';", $result );
  204. break;
  205. case 'Remote Stylesheet 3':
  206. $this->assertEquals( '&lt;META HTTP-EQUIV=&quot;Link&quot; Content=&quot;; REL=stylesheet"&gt;', $result );
  207. break;
  208. case 'Remote Stylesheet 4':
  209. $this->assertEquals('BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}', $result);
  210. break;
  211. case 'XML data island w/CDATA':
  212. $this->assertEquals( "&lt;![CDATA[]]&gt;", $result );
  213. break;
  214. case 'XML data island w/comment':
  215. $this->assertEquals( "<I><B>&lt;IMG SRC=&quot;javas<!-- -->cript:alert('XSS')\"&gt;</B></I>", $result );
  216. break;
  217. case 'XML HTML+TIME':
  218. $this->assertEquals( '&lt;t:set attributeName=&quot;innerHTML&quot; to=&quot;XSSalert(\'XSS\')"&gt;', $result );
  219. break;
  220. case 'Commented-out Block':
  221. $this->assertEquals( "<!--[if gte IE 4]&gt;-->\nalert('XSS');", $result );
  222. break;
  223. case 'Cookie Manipulation':
  224. $this->assertEquals( '&lt;META HTTP-EQUIV=&quot;Set-Cookie&quot; Content=&quot;USERID=alert(\'XSS\')"&gt;', $result );
  225. break;
  226. case 'SSI':
  227. $this->assertEquals( '&lt;!--#exec cmd=&quot;/bin/echo &#039;<!--#exec cmd="/bin/echo \'=http://ha.ckers.org/xss.js&gt;\'"-->', $result );
  228. break;
  229. case 'PHP':
  230. $this->assertEquals( '&lt;? echo(&#039;alert("XSS")\'); ?&gt;', $result );
  231. break;
  232. case 'UTF-7 Encoding':
  233. $this->assertEquals( '+ADw-SCRIPT+AD4-alert(\'XSS\');+ADw-/SCRIPT+AD4-', $result );
  234. break;
  235. case 'Escaping JavaScript escapes':
  236. $this->assertEquals('\";alert(\'XSS\');//', $result);
  237. break;
  238. case 'STYLE w/broken up JavaScript':
  239. $this->assertEquals( '@im\port\'\ja\vasc\ript:alert("XSS")\';', $result );
  240. break;
  241. case 'Null Chars 2':
  242. $this->assertEquals( '&amp;alert("XSS")', $result );
  243. break;
  244. case 'No Closing Script Tag':
  245. $this->assertEquals( '&lt;SCRIPT SRC=http://ha.ckers.org/xss.js', $result );
  246. break;
  247. case 'Half-Open HTML/JavaScript':
  248. $this->assertEquals( '&lt;IMG SRC=&quot;javascript:alert(&#039;XSS&#039;)&quot;', $result );
  249. break;
  250. case 'Double open angle brackets':
  251. $this->assertEquals( '&lt;IFRAME SRC=http://ha.ckers.org/scriptlet.html &lt;', $result );
  252. break;
  253. case 'Extraneous Open Brackets':
  254. $this->assertEquals( '&lt;alert("XSS");//&lt;', $result );
  255. break;
  256. case 'Malformed IMG Tags':
  257. $this->assertEquals('alert("XSS")"&gt;', $result);
  258. break;
  259. case 'No Quotes/Semicolons':
  260. $this->assertEquals( "a=/XSS/\nalert(a.source)", $result );
  261. break;
  262. case 'Evade Regex Filter 1':
  263. $this->assertEquals( '" SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  264. break;
  265. case 'Evade Regex Filter 4':
  266. $this->assertEquals( '\'" SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  267. break;
  268. case 'Evade Regex Filter 5':
  269. $this->assertEquals( '` SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  270. break;
  271. case 'Filter Evasion 1':
  272. $this->assertEquals( 'document.write("&lt;SCRI&quot;);PT SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  273. break;
  274. case 'Filter Evasion 2':
  275. $this->assertEquals( '\'&gt;" SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  276. break;
  277. default:
  278. $this->fail( 'KSES failed on ' . $attack->name . ': ' . $result );
  279. }
  280. }
  281. }
  282. function _wp_kses_allowed_html_filter( $html, $context ) {
  283. if ( 'post' == $context )
  284. return array( 'a' => array( 'href' => true ) );
  285. else
  286. return array( 'a' => array( 'href' => false ) );
  287. }
  288. /**
  289. * @ticket 20210
  290. */
  291. public function test_wp_kses_allowed_html() {
  292. global $allowedposttags, $allowedtags, $allowedentitynames;
  293. $this->assertEquals( $allowedposttags, wp_kses_allowed_html( 'post' ) );
  294. $tags = wp_kses_allowed_html( 'post' ) ;
  295. foreach ( $tags as $tag ) {
  296. $this->assertTrue( $tag['class'] );
  297. $this->assertTrue( $tag['id'] );
  298. $this->assertTrue( $tag['style'] );
  299. $this->assertTrue( $tag['title'] );
  300. }
  301. $this->assertEquals( $allowedtags, wp_kses_allowed_html( 'data' ) );
  302. $this->assertEquals( $allowedtags, wp_kses_allowed_html( '' ) );
  303. $this->assertEquals( $allowedtags, wp_kses_allowed_html() );
  304. $tags = wp_kses_allowed_html( 'user_description' );
  305. $this->assertTrue( $tags['a']['rel'] );
  306. $tags = wp_kses_allowed_html();
  307. $this->assertFalse( isset( $tags['a']['rel'] ) );
  308. $this->assertEquals( array(), wp_kses_allowed_html( 'strip' ) );
  309. $custom_tags = array(
  310. 'a' => array(
  311. 'href' => true,
  312. 'rel' => true,
  313. 'rev' => true,
  314. 'name' => true,
  315. 'target' => true,
  316. ),
  317. );
  318. $this->assertEquals( $custom_tags, wp_kses_allowed_html( $custom_tags ) );
  319. add_filter( 'wp_kses_allowed_html', array( $this, '_wp_kses_allowed_html_filter' ), 10, 2 );
  320. $this->assertEquals( array( 'a' => array( 'href' => true ) ), wp_kses_allowed_html( 'post' ) );
  321. $this->assertEquals( array( 'a' => array( 'href' => false ) ), wp_kses_allowed_html( 'data' ) );
  322. remove_filter( 'wp_kses_allowed_html', array( $this, '_wp_kses_allowed_html_filter' ) );
  323. $this->assertEquals( $allowedposttags, wp_kses_allowed_html( 'post' ) );
  324. $this->assertEquals( $allowedtags, wp_kses_allowed_html( 'data' ) );
  325. }
  326. /**
  327. * @ticket 26290
  328. */
  329. public function test_wp_kses_normalize_entities() {
  330. $this->assertEquals( '&spades;', wp_kses_normalize_entities( '&spades;' ) );
  331. $this->assertEquals( '&sup1;', wp_kses_normalize_entities( '&sup1;' ) );
  332. $this->assertEquals( '&sup2;', wp_kses_normalize_entities( '&sup2;' ) );
  333. $this->assertEquals( '&sup3;', wp_kses_normalize_entities( '&sup3;' ) );
  334. $this->assertEquals( '&frac14;', wp_kses_normalize_entities( '&frac14;' ) );
  335. $this->assertEquals( '&frac12;', wp_kses_normalize_entities( '&frac12;' ) );
  336. $this->assertEquals( '&frac34;', wp_kses_normalize_entities( '&frac34;' ) );
  337. $this->assertEquals( '&there4;', wp_kses_normalize_entities( '&there4;' ) );
  338. }
  339. }