kses.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449
  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. * @dataProvider data_wp_filter_post_kses_address
  11. * @ticket 20210
  12. *
  13. * @param string $string Test string for kses.
  14. * @param string $expect_string Expected result after passing through kses.
  15. */
  16. function test_wp_filter_post_kses_address( $string, $expect_string ) {
  17. global $allowedposttags;
  18. $this->assertSame( $expect_string, wp_kses( $string, $allowedposttags ) );
  19. }
  20. /**
  21. * Data provider for test_wp_filter_post_kses_address.
  22. *
  23. * @return array[] Arguments {
  24. * @type string $string Test string for kses.
  25. * @type string $expect_string Expected result after passing through kses.
  26. * }
  27. */
  28. function data_wp_filter_post_kses_address() {
  29. $attributes = array(
  30. 'class' => 'classname',
  31. 'id' => 'id',
  32. 'style' => array(
  33. 'color: red;',
  34. 'color: red',
  35. 'color: red; text-align:center',
  36. 'color: red; text-align:center;',
  37. ),
  38. 'title' => 'title',
  39. );
  40. $data = array();
  41. foreach ( $attributes as $name => $values ) {
  42. foreach ( (array) $values as $value ) {
  43. $string = "<address $name='$value'>1 WordPress Avenue, The Internet.</address>";
  44. $expect_string = "<address $name='" . str_replace( '; ', ';', trim( $value, ';' ) ) . "'>1 WordPress Avenue, The Internet.</address>";
  45. $data[] = array( $string, $expect_string );
  46. }
  47. }
  48. return $data;
  49. }
  50. /**
  51. * @dataProvider data_wp_filter_post_kses_a
  52. * @ticket 20210
  53. *
  54. * @param string $string Test string for kses.
  55. * @param string $expect_string Expected result after passing through kses.
  56. * @return void
  57. */
  58. function test_wp_filter_post_kses_a( $string, $expect_string ) {
  59. global $allowedposttags;
  60. $this->assertSame( $expect_string, wp_kses( $string, $allowedposttags ) );
  61. }
  62. /**
  63. * Data provider for test_wp_filter_post_kses_a.
  64. *
  65. * @return array[] Arguments {
  66. * @type string $string Test string for kses.
  67. * @type string $expect_string Expected result after passing through kses.
  68. * }
  69. */
  70. function data_wp_filter_post_kses_a() {
  71. $attributes = array(
  72. 'class' => 'classname',
  73. 'id' => 'id',
  74. 'style' => 'color: red;',
  75. 'title' => 'title',
  76. 'href' => 'http://example.com',
  77. 'rel' => 'related',
  78. 'rev' => 'revision',
  79. 'name' => 'name',
  80. 'target' => '_blank',
  81. 'download' => '',
  82. );
  83. $data = array();
  84. foreach ( $attributes as $name => $value ) {
  85. if ( $value ) {
  86. $attr = "$name='$value'";
  87. $expected_attr = "$name='" . trim( $value, ';' ) . "'";
  88. } else {
  89. $attr = $name;
  90. $expected_attr = $name;
  91. }
  92. $string = "<a $attr>I link this</a>";
  93. $expect_string = "<a $expected_attr>I link this</a>";
  94. $data[] = array( $string, $expect_string );
  95. }
  96. return $data;
  97. }
  98. /**
  99. * Test video tag.
  100. *
  101. * @ticket 50167
  102. * @ticket 29826
  103. * @dataProvider data_wp_kses_video
  104. *
  105. * @param string $source Source HTML.
  106. * @param string $context Context to use for parsing source.
  107. * @param string $expected Expected output following KSES parsing.
  108. */
  109. function test_wp_kses_video( $source, $context, $expected ) {
  110. $actual = wp_kses( $source, $context );
  111. $this->assertSame( $expected, $actual );
  112. }
  113. /**
  114. * Data provider for test_wp_kses_video
  115. *
  116. * @return array[] Array containing test data {
  117. * @type string $source Source HTML.
  118. * @type string $context Context to use for parsing source.
  119. * @type string $expected Expected output following KSES parsing.
  120. * }
  121. */
  122. function data_wp_kses_video() {
  123. return array(
  124. // Set 0: Valid post object params in post context.
  125. array(
  126. '<video src="movie.mov" autoplay controls height=9 loop muted poster="still.gif" playsinline preload width=16 />',
  127. 'post',
  128. '<video src="movie.mov" autoplay controls height="9" loop muted poster="still.gif" playsinline preload width="16" />',
  129. ),
  130. // Set 1: Valid post object params in data context.
  131. array(
  132. '<video src="movie.mov" autoplay controls height=9 loop muted poster="still.gif" playsinline preload width=16 />',
  133. 'data',
  134. '',
  135. ),
  136. // Set 2: Disallowed urls in post context.
  137. array(
  138. '<video src="bad://w.org/movie.mov" poster="bad://w.org/movie.jpg" />',
  139. 'post',
  140. '<video src="//w.org/movie.mov" poster="//w.org/movie.jpg" />',
  141. ),
  142. // Set 3: Disallowed attributes in post context.
  143. array(
  144. '<video onload="alert(1);" src="https://videos.files.wordpress.com/DZEMDKxc/video-0f9c363010.mp4" />',
  145. 'post',
  146. '<video src="https://videos.files.wordpress.com/DZEMDKxc/video-0f9c363010.mp4" />',
  147. ),
  148. );
  149. }
  150. /**
  151. * @dataProvider data_wp_filter_post_kses_abbr
  152. * @ticket 20210
  153. *
  154. * @param string $string Test string for kses.
  155. * @param string $expect_string Expected result after passing through kses.
  156. * @return void
  157. */
  158. function test_wp_filter_post_kses_abbr( $string, $expect_string ) {
  159. global $allowedposttags;
  160. $this->assertSame( $expect_string, wp_kses( $string, $allowedposttags ) );
  161. }
  162. /**
  163. * Data provider for data_wp_filter_post_kses_abbr.
  164. *
  165. * @return array[] Arguments {
  166. * @type string $string Test string for kses.
  167. * @type string $expect_string Expected result after passing through kses.
  168. * }
  169. */
  170. function data_wp_filter_post_kses_abbr() {
  171. $attributes = array(
  172. 'class' => 'classname',
  173. 'id' => 'id',
  174. 'style' => 'color: red;',
  175. 'title' => 'title',
  176. );
  177. $data = array();
  178. foreach ( $attributes as $name => $value ) {
  179. $string = "<abbr $name='$value'>WP</abbr>";
  180. $expect_string = "<abbr $name='" . trim( $value, ';' ) . "'>WP</abbr>";
  181. $data[] = array( $string, $expect_string );
  182. }
  183. return $data;
  184. }
  185. function test_feed_links() {
  186. global $allowedposttags;
  187. $content = <<<EOF
  188. <a href="feed:javascript:alert(1)">CLICK ME</a>
  189. <a href="feed:javascript:feed:alert(1)">CLICK ME</a>
  190. <a href="feed:feed:javascript:alert(1)">CLICK ME</a>
  191. <a href="javascript:feed:alert(1)">CLICK ME</a>
  192. <a href="javascript:feed:javascript:alert(1)">CLICK ME</a>
  193. <a href="feed:feed:feed:javascript:alert(1)">CLICK ME</a>
  194. <a href="feed:feed:feed:feed:javascript:alert(1)">CLICK ME</a>
  195. <a href="feed:feed:feed:feed:feed:javascript:alert(1)">CLICK ME</a>
  196. <a href="feed:javascript:feed:javascript:feed:javascript:alert(1)">CLICK ME</a>
  197. <a href="feed:javascript:feed:javascript:feed:javascript:feed:javascript:feed:javascript:alert(1)">CLICK ME</a>
  198. <a href="feed:feed:feed:http:alert(1)">CLICK ME</a>
  199. EOF;
  200. $expected = <<<EOF
  201. <a href="feed:alert(1)">CLICK ME</a>
  202. <a href="feed:feed:alert(1)">CLICK ME</a>
  203. <a href="feed:feed:alert(1)">CLICK ME</a>
  204. <a href="feed:alert(1)">CLICK ME</a>
  205. <a href="feed:alert(1)">CLICK ME</a>
  206. <a href="">CLICK ME</a>
  207. <a href="">CLICK ME</a>
  208. <a href="">CLICK ME</a>
  209. <a href="">CLICK ME</a>
  210. <a href="">CLICK ME</a>
  211. <a href="">CLICK ME</a>
  212. EOF;
  213. $this->assertSame( $expected, wp_kses( $content, $allowedposttags ) );
  214. }
  215. function test_wp_kses_bad_protocol() {
  216. $bad = array(
  217. 'dummy:alert(1)',
  218. 'javascript:alert(1)',
  219. 'JaVaScRiPt:alert(1)',
  220. 'javascript:alert(1);',
  221. 'javascript&#58;alert(1);',
  222. 'javascript&#0058;alert(1);',
  223. 'javascript&#0000058alert(1);',
  224. 'javascript&#x3A;alert(1);',
  225. 'javascript&#X3A;alert(1);',
  226. 'javascript&#X3a;alert(1);',
  227. 'javascript&#x3a;alert(1);',
  228. 'javascript&#x003a;alert(1);',
  229. '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
  230. 'jav ascript:alert(1);',
  231. 'jav&#x09;ascript:alert(1);',
  232. 'jav&#x0A;ascript:alert(1);',
  233. 'jav&#x0D;ascript:alert(1);',
  234. ' &#14; javascript:alert(1);',
  235. 'javascript:javascript:alert(1);',
  236. 'javascript&#58;javascript:alert(1);',
  237. 'javascript&#0000058javascript:alert(1);',
  238. 'javascript:javascript&#58;alert(1);',
  239. 'javascript:javascript&#0000058alert(1);',
  240. 'javascript&#0000058alert(1)//?:',
  241. 'feed:javascript:alert(1)',
  242. 'feed:javascript:feed:javascript:feed:javascript:alert(1)',
  243. 'javascript&#58alert(1)',
  244. 'javascript&#x3ax=1;alert(1)',
  245. );
  246. foreach ( $bad as $k => $x ) {
  247. $result = wp_kses_bad_protocol( wp_kses_normalize_entities( $x ), wp_allowed_protocols() );
  248. if ( ! empty( $result ) && 'alert(1);' !== $result && 'alert(1)' !== $result ) {
  249. switch ( $k ) {
  250. case 6:
  251. $this->assertSame( 'javascript&amp;#0000058alert(1);', $result );
  252. break;
  253. case 12:
  254. $this->assertSame( str_replace( '&', '&amp;', $x ), $result );
  255. break;
  256. case 22:
  257. $this->assertSame( 'javascript&amp;#0000058alert(1);', $result );
  258. break;
  259. case 23:
  260. $this->assertSame( 'javascript&amp;#0000058alert(1)//?:', $result );
  261. break;
  262. case 24:
  263. $this->assertSame( 'feed:alert(1)', $result );
  264. break;
  265. case 26:
  266. $this->assertSame( 'javascript&amp;#58alert(1)', $result );
  267. break;
  268. case 27:
  269. $this->assertSame( 'javascript&amp;#x3ax=1;alert(1)', $result );
  270. break;
  271. default:
  272. $this->fail( "wp_kses_bad_protocol failed on $k, $x. Result: $result" );
  273. }
  274. }
  275. }
  276. $bad_not_normalized = array(
  277. 'dummy&colon;alert(1)',
  278. 'javascript&colon;alert(1)',
  279. 'javascript&CoLon;alert(1)',
  280. 'javascript&COLON;alert(1);',
  281. 'javascript&#58;alert(1);',
  282. 'javascript&#0058;alert(1);',
  283. 'javascript&#0000058alert(1);',
  284. 'jav ascript&COLON;alert(1);',
  285. 'javascript&#58;javascript&colon;alert(1);',
  286. 'javascript&#58;javascript&colon;alert(1);',
  287. 'javascript&#0000058javascript&colon;alert(1);',
  288. 'javascript&#58;javascript&#0000058alert(1);',
  289. 'javascript&#58alert(1)',
  290. );
  291. foreach ( $bad_not_normalized as $k => $x ) {
  292. $result = wp_kses_bad_protocol( $x, wp_allowed_protocols() );
  293. if ( ! empty( $result ) && 'alert(1);' !== $result && 'alert(1)' !== $result ) {
  294. $this->fail( "wp_kses_bad_protocol failed on $k, $x. Result: $result" );
  295. }
  296. }
  297. $safe = array(
  298. 'dummy:alert(1)',
  299. 'HTTP://example.org/',
  300. 'http://example.org/',
  301. 'http&#58;//example.org/',
  302. 'http&#x3A;//example.org/',
  303. 'https://example.org',
  304. 'http://example.org/wp-admin/post.php?post=2&amp;action=edit',
  305. 'http://example.org/index.php?test=&#039;blah&#039;',
  306. );
  307. foreach ( $safe as $x ) {
  308. $result = wp_kses_bad_protocol( wp_kses_normalize_entities( $x ), array( 'http', 'https', 'dummy' ) );
  309. if ( $result !== $x && 'http://example.org/' !== $result ) {
  310. $this->fail( "wp_kses_bad_protocol incorrectly blocked $x" );
  311. }
  312. }
  313. }
  314. public function test_hackers_attacks() {
  315. $xss = simplexml_load_file( DIR_TESTDATA . '/formatting/xssAttacks.xml' );
  316. foreach ( $xss->attack as $attack ) {
  317. if ( in_array( (string) $attack->name, array( 'IMG Embedded commands 2', 'US-ASCII encoding', 'OBJECT w/Flash 2', 'Character Encoding Example' ), true ) ) {
  318. continue;
  319. }
  320. $code = (string) $attack->code;
  321. if ( 'See Below' === $code ) {
  322. continue;
  323. }
  324. if ( substr( $code, 0, 4 ) === 'perl' ) {
  325. $pos = strpos( $code, '"' ) + 1;
  326. $code = substr( $code, $pos, strrpos( $code, '"' ) - $pos );
  327. $code = str_replace( '\0', "\0", $code );
  328. }
  329. $result = trim( wp_kses_data( $code ) );
  330. if ( in_array( $result, array( '', 'XSS', 'alert("XSS");', "alert('XSS');" ), true ) ) {
  331. continue;
  332. }
  333. switch ( $attack->name ) {
  334. case 'XSS Locator':
  335. $this->assertSame( '\';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))=&amp;{}', $result );
  336. break;
  337. case 'XSS Quick Test':
  338. $this->assertSame( '\'\';!--"=&amp;{()}', $result );
  339. break;
  340. case 'SCRIPT w/Alert()':
  341. $this->assertSame( "alert('XSS')", $result );
  342. break;
  343. case 'SCRIPT w/Char Code':
  344. $this->assertSame( 'alert(String.fromCharCode(88,83,83))', $result );
  345. break;
  346. case 'IMG STYLE w/expression':
  347. $this->assertSame( 'exp/*', $result );
  348. break;
  349. case 'List-style-image':
  350. $this->assertSame( 'li {list-style-image: url("javascript:alert(\'XSS\')");}XSS', $result );
  351. break;
  352. case 'STYLE':
  353. $this->assertSame( "alert('XSS');", $result );
  354. break;
  355. case 'STYLE w/background-image':
  356. $this->assertSame( '.XSS{background-image:url("javascript:alert(\'XSS\')");}<A></A>', $result );
  357. break;
  358. case 'STYLE w/background':
  359. $this->assertSame( 'BODY{background:url("javascript:alert(\'XSS\')")}', $result );
  360. break;
  361. case 'Remote Stylesheet 2':
  362. $this->assertSame( "@import'http://ha.ckers.org/xss.css';", $result );
  363. break;
  364. case 'Remote Stylesheet 3':
  365. $this->assertSame( '&lt;META HTTP-EQUIV=&quot;Link&quot; Content=&quot;; REL=stylesheet"&gt;', $result );
  366. break;
  367. case 'Remote Stylesheet 4':
  368. $this->assertSame( 'BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}', $result );
  369. break;
  370. case 'XML data island w/CDATA':
  371. $this->assertSame( '&lt;![CDATA[]]&gt;', $result );
  372. break;
  373. case 'XML data island w/comment':
  374. $this->assertSame( "<I><B>&lt;IMG SRC=&quot;javas<!-- -->cript:alert('XSS')\"&gt;</B></I>", $result );
  375. break;
  376. case 'XML HTML+TIME':
  377. $this->assertSame( '&lt;t:set attributeName=&quot;innerHTML&quot; to=&quot;XSSalert(\'XSS\')"&gt;', $result );
  378. break;
  379. case 'Commented-out Block':
  380. $this->assertSame( "<!--[if gte IE 4]&gt;-->\nalert('XSS');", $result );
  381. break;
  382. case 'Cookie Manipulation':
  383. $this->assertSame( '&lt;META HTTP-EQUIV=&quot;Set-Cookie&quot; Content=&quot;USERID=alert(\'XSS\')"&gt;', $result );
  384. break;
  385. case 'SSI':
  386. $this->assertSame( '&lt;!--#exec cmd=&quot;/bin/echo &#039;<!--#exec cmd="/bin/echo \'=http://ha.ckers.org/xss.js&gt;\'"-->', $result );
  387. break;
  388. case 'PHP':
  389. $this->assertSame( '&lt;? echo(&#039;alert("XSS")\'); ?&gt;', $result );
  390. break;
  391. case 'UTF-7 Encoding':
  392. $this->assertSame( '+ADw-SCRIPT+AD4-alert(\'XSS\');+ADw-/SCRIPT+AD4-', $result );
  393. break;
  394. case 'Escaping JavaScript escapes':
  395. $this->assertSame( '\";alert(\'XSS\');//', $result );
  396. break;
  397. case 'STYLE w/broken up JavaScript':
  398. $this->assertSame( '@im\port\'\ja\vasc\ript:alert("XSS")\';', $result );
  399. break;
  400. case 'Null Chars 2':
  401. $this->assertSame( '&amp;alert("XSS")', $result );
  402. break;
  403. case 'No Closing Script Tag':
  404. $this->assertSame( '&lt;SCRIPT SRC=http://ha.ckers.org/xss.js', $result );
  405. break;
  406. case 'Half-Open HTML/JavaScript':
  407. $this->assertSame( '&lt;IMG SRC=&quot;javascript:alert(&#039;XSS&#039;)&quot;', $result );
  408. break;
  409. case 'Double open angle brackets':
  410. $this->assertSame( '&lt;IFRAME SRC=http://ha.ckers.org/scriptlet.html &lt;', $result );
  411. break;
  412. case 'Extraneous Open Brackets':
  413. $this->assertSame( '&lt;alert("XSS");//&lt;', $result );
  414. break;
  415. case 'Malformed IMG Tags':
  416. $this->assertSame( 'alert("XSS")"&gt;', $result );
  417. break;
  418. case 'No Quotes/Semicolons':
  419. $this->assertSame( "a=/XSS/\nalert(a.source)", $result );
  420. break;
  421. case 'Evade Regex Filter 1':
  422. $this->assertSame( '" SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  423. break;
  424. case 'Evade Regex Filter 4':
  425. $this->assertSame( '\'" SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  426. break;
  427. case 'Evade Regex Filter 5':
  428. $this->assertSame( '` SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  429. break;
  430. case 'Filter Evasion 1':
  431. $this->assertSame( 'document.write("&lt;SCRI&quot;);PT SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  432. break;
  433. case 'Filter Evasion 2':
  434. $this->assertSame( '\'&gt;" SRC="http://ha.ckers.org/xss.js"&gt;', $result );
  435. break;
  436. default:
  437. $this->fail( 'KSES failed on ' . $attack->name . ': ' . $result );
  438. }
  439. }
  440. }
  441. function _wp_kses_allowed_html_filter( $html, $context ) {
  442. if ( 'post' === $context ) {
  443. return array( 'a' => array( 'href' => true ) );
  444. } else {
  445. return array( 'a' => array( 'href' => false ) );
  446. }
  447. }
  448. /**
  449. * @ticket 20210
  450. */
  451. public function test_wp_kses_allowed_html() {
  452. global $allowedposttags, $allowedtags, $allowedentitynames;
  453. $this->assertSame( $allowedposttags, wp_kses_allowed_html( 'post' ) );
  454. $tags = wp_kses_allowed_html( 'post' );
  455. foreach ( $tags as $tag ) {
  456. $this->assertTrue( $tag['class'] );
  457. $this->assertTrue( $tag['id'] );
  458. $this->assertTrue( $tag['style'] );
  459. $this->assertTrue( $tag['title'] );
  460. }
  461. $this->assertSame( $allowedtags, wp_kses_allowed_html( 'data' ) );
  462. $this->assertSame( $allowedtags, wp_kses_allowed_html( '' ) );
  463. $this->assertSame( $allowedtags, wp_kses_allowed_html() );
  464. $tags = wp_kses_allowed_html( 'user_description' );
  465. $this->assertTrue( $tags['a']['rel'] );
  466. $tags = wp_kses_allowed_html();
  467. $this->assertFalse( isset( $tags['a']['rel'] ) );
  468. $this->assertSame( array(), wp_kses_allowed_html( 'strip' ) );
  469. $custom_tags = array(
  470. 'a' => array(
  471. 'href' => true,
  472. 'rel' => true,
  473. 'rev' => true,
  474. 'name' => true,
  475. 'target' => true,
  476. ),
  477. );
  478. $this->assertSame( $custom_tags, wp_kses_allowed_html( $custom_tags ) );
  479. add_filter( 'wp_kses_allowed_html', array( $this, '_wp_kses_allowed_html_filter' ), 10, 2 );
  480. $this->assertSame( array( 'a' => array( 'href' => true ) ), wp_kses_allowed_html( 'post' ) );
  481. $this->assertSame( array( 'a' => array( 'href' => false ) ), wp_kses_allowed_html( 'data' ) );
  482. remove_filter( 'wp_kses_allowed_html', array( $this, '_wp_kses_allowed_html_filter' ) );
  483. $this->assertSame( $allowedposttags, wp_kses_allowed_html( 'post' ) );
  484. $this->assertSame( $allowedtags, wp_kses_allowed_html( 'data' ) );
  485. }
  486. function test_hyphenated_tag() {
  487. $string = '<hyphenated-tag attribute="value" otherattribute="value2">Alot of hyphens.</hyphenated-tag>';
  488. $custom_tags = array(
  489. 'hyphenated-tag' => array(
  490. 'attribute' => true,
  491. ),
  492. );
  493. $expect_stripped_string = 'Alot of hyphens.';
  494. $expect_valid_string = '<hyphenated-tag attribute="value">Alot of hyphens.</hyphenated-tag>';
  495. $this->assertSame( $expect_stripped_string, wp_kses_post( $string ) );
  496. $this->assertSame( $expect_valid_string, wp_kses( $string, $custom_tags ) );
  497. }
  498. /**
  499. * @ticket 26290
  500. */
  501. public function test_wp_kses_normalize_entities() {
  502. $this->assertSame( '&spades;', wp_kses_normalize_entities( '&spades;' ) );
  503. $this->assertSame( '&sup1;', wp_kses_normalize_entities( '&sup1;' ) );
  504. $this->assertSame( '&sup2;', wp_kses_normalize_entities( '&sup2;' ) );
  505. $this->assertSame( '&sup3;', wp_kses_normalize_entities( '&sup3;' ) );
  506. $this->assertSame( '&frac14;', wp_kses_normalize_entities( '&frac14;' ) );
  507. $this->assertSame( '&frac12;', wp_kses_normalize_entities( '&frac12;' ) );
  508. $this->assertSame( '&frac34;', wp_kses_normalize_entities( '&frac34;' ) );
  509. $this->assertSame( '&there4;', wp_kses_normalize_entities( '&there4;' ) );
  510. }
  511. /**
  512. * Test removal of invalid binary data for HTML.
  513. *
  514. * @ticket 28506
  515. * @dataProvider data_ctrl_removal
  516. */
  517. function test_ctrl_removal( $input, $output ) {
  518. global $allowedposttags;
  519. return $this->assertSame( $output, wp_kses( $input, $allowedposttags ) );
  520. }
  521. function data_ctrl_removal() {
  522. return array(
  523. array(
  524. "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\X1C\x1D\x1E\x1F",
  525. '',
  526. ),
  527. array(
  528. "\x00h\x01e\x02l\x03l\x04o\x05 \x06w\x07o\x08r\x0Bl\x0Cd\x0E.\x0F \x10W\x11O\x12R\x13D\x14P\x15R\x16E\x17S\x18S\x19 \x1AK\x1BS\X1CE\x1DS\x1E.\x1F/",
  529. 'hello world. WORDPRESS KSES./',
  530. ),
  531. array(
  532. "\x1F\x1E\x1D\x1C\x1B\x1A\x19\x18\x17\x16\x15\x14\x13\x12\x11\x10\x0F\x0E\x0C\x0B\x08\x07\x06\x05\x04\X03\x02\x01\x00",
  533. '',
  534. ),
  535. array(
  536. "\x1Fh\x1Ee\x1Dl\x1Cl\x1Bo\x1A \x19w\x18o\x17r\x16l\x15d\x14.\x13 \x12W\x11O\x10R\x0FD\x0EP\x0CR\x0BE\x08S\x07S\x06 \x05K\x04S\X03E\x02S\x01.\x00/",
  537. 'hello world. WORDPRESS KSES./',
  538. ),
  539. array(
  540. "\t\r\n word \n\r\t",
  541. "\t\r\n word \n\r\t",
  542. ),
  543. );
  544. }
  545. /**
  546. * Test removal of '\0' strings.
  547. *
  548. * @ticket 28699
  549. * @dataProvider data_slash_zero_removal
  550. */
  551. function test_slash_zero_removal( $input, $output ) {
  552. global $allowedposttags;
  553. return $this->assertSame( $output, wp_kses( $input, $allowedposttags ) );
  554. }
  555. function data_slash_zero_removal() {
  556. return array(
  557. array(
  558. 'This \\0 should be no big deal.',
  559. 'This \\0 should be no big deal.',
  560. ),
  561. array(
  562. '<div>This \\0 should be no big deal.</div>',
  563. '<div>This \\0 should be no big deal.</div>',
  564. ),
  565. array(
  566. '<div align="\\0left">This should be no big deal.</div>',
  567. '<div align="\\0left">This should be no big deal.</div>',
  568. ),
  569. array(
  570. 'This <div style="float:\\0left"> is more of a concern.',
  571. 'This <div style="float:left"> is more of a concern.',
  572. ),
  573. array(
  574. 'This <div style="float:\\0\\0left"> is more of a concern.',
  575. 'This <div style="float:left"> is more of a concern.',
  576. ),
  577. array(
  578. 'This <div style="float:\\\\00left"> is more of a concern.',
  579. 'This <div style="float:left"> is more of a concern.',
  580. ),
  581. array(
  582. 'This <div style="float:\\\\\\\\0000left"> is more of a concern.',
  583. 'This <div style="float:left"> is more of a concern.',
  584. ),
  585. array(
  586. 'This <div style="float:\\0000left"> is more of a concern.',
  587. 'This <div style="float:left"> is more of a concern.',
  588. ),
  589. array(
  590. '<style type="text/css">div {background-image:\\0}</style>',
  591. 'div {background-image:\\0}',
  592. ),
  593. );
  594. }
  595. /**
  596. * Test new function wp_kses_hair_parse().
  597. *
  598. * @dataProvider data_hair_parse
  599. */
  600. function test_hair_parse( $input, $output ) {
  601. return $this->assertSame( $output, wp_kses_hair_parse( $input ) );
  602. }
  603. function data_hair_parse() {
  604. return array(
  605. array(
  606. 'title="hello" href="#" id="my_id" ',
  607. array( 'title="hello" ', 'href="#" ', 'id="my_id" ' ),
  608. ),
  609. array(
  610. '[shortcode attr="value"] href="http://www.google.com/"title="moo"disabled',
  611. array( '[shortcode attr="value"] ', 'href="http://www.google.com/"', 'title="moo"', 'disabled' ),
  612. ),
  613. array(
  614. '',
  615. array(),
  616. ),
  617. array(
  618. 'a',
  619. array( 'a' ),
  620. ),
  621. array(
  622. 'title="hello"disabled href=# id=\'my_id\'',
  623. array( 'title="hello"', 'disabled ', 'href=# ', "id='my_id'" ),
  624. ),
  625. array(
  626. ' ', // Calling function is expected to strip leading whitespace.
  627. false,
  628. ),
  629. array(
  630. 'abcd=abcd"abcd"',
  631. false,
  632. ),
  633. array(
  634. "array[1]='z'z'z'z",
  635. false,
  636. ),
  637. // Using a digit in attribute name should work.
  638. array(
  639. 'href="https://example.com/[shortcode attr=\'value\']" data-op3-timer-seconds="0"',
  640. array( 'href="https://example.com/[shortcode attr=\'value\']" ', 'data-op3-timer-seconds="0"' ),
  641. ),
  642. // Using an underscore in attribute name should work.
  643. array(
  644. 'href="https://example.com/[shortcode attr=\'value\']" data-op_timer-seconds="0"',
  645. array( 'href="https://example.com/[shortcode attr=\'value\']" ', 'data-op_timer-seconds="0"' ),
  646. ),
  647. // Using a period in attribute name should work.
  648. array(
  649. 'href="https://example.com/[shortcode attr=\'value\']" data-op.timer-seconds="0"',
  650. array( 'href="https://example.com/[shortcode attr=\'value\']" ', 'data-op.timer-seconds="0"' ),
  651. ),
  652. // Using a digit at the beginning of attribute name should return false.
  653. array(
  654. 'href="https://example.com/[shortcode attr=\'value\']" 3data-op-timer-seconds="0"',
  655. false,
  656. ),
  657. );
  658. }
  659. /**
  660. * Test new function wp_kses_attr_parse().
  661. *
  662. * @dataProvider data_attr_parse
  663. */
  664. function test_attr_parse( $input, $output ) {
  665. return $this->assertSame( $output, wp_kses_attr_parse( $input ) );
  666. }
  667. function data_attr_parse() {
  668. return array(
  669. array(
  670. '<a title="hello" href="#" id="my_id" >',
  671. array( '<a ', 'title="hello" ', 'href="#" ', 'id="my_id" ', '>' ),
  672. ),
  673. array(
  674. '<a [shortcode attr="value"] href="http://www.google.com/"title="moo"disabled>',
  675. array( '<a ', '[shortcode attr="value"] ', 'href="http://www.google.com/"', 'title="moo"', 'disabled', '>' ),
  676. ),
  677. array(
  678. '',
  679. false,
  680. ),
  681. array(
  682. 'a',
  683. false,
  684. ),
  685. array(
  686. '<a>',
  687. array( '<a', '>' ),
  688. ),
  689. array(
  690. '<a%%&&**>',
  691. false,
  692. ),
  693. array(
  694. '<a title="hello"disabled href=# id=\'my_id\'>',
  695. array( '<a ', 'title="hello"', 'disabled ', 'href=# ', "id='my_id'", '>' ),
  696. ),
  697. array(
  698. '<a >',
  699. array( '<a ', '>' ),
  700. ),
  701. array(
  702. '<a abcd=abcd"abcd">',
  703. false,
  704. ),
  705. array(
  706. "<a array[1]='z'z'z'z>",
  707. false,
  708. ),
  709. array(
  710. '<img title="hello" src="#" id="my_id" />',
  711. array( '<img ', 'title="hello" ', 'src="#" ', 'id="my_id"', ' />' ),
  712. ),
  713. );
  714. }
  715. /**
  716. * Test new function wp_kses_one_attr().
  717. *
  718. * @dataProvider data_one_attr
  719. */
  720. function test_one_attr( $element, $input, $output ) {
  721. return $this->assertSame( $output, wp_kses_one_attr( $input, $element ) );
  722. }
  723. function data_one_attr() {
  724. return array(
  725. array(
  726. 'a',
  727. ' title="hello" ',
  728. ' title="hello" ',
  729. ),
  730. array(
  731. 'a',
  732. 'title = "hello"',
  733. 'title="hello"',
  734. ),
  735. array(
  736. 'a',
  737. "title='hello'",
  738. "title='hello'",
  739. ),
  740. array(
  741. 'a',
  742. 'title=hello',
  743. 'title="hello"',
  744. ),
  745. array(
  746. 'a',
  747. 'href="javascript:alert(1)"',
  748. 'href="alert(1)"',
  749. ),
  750. array(
  751. 'a',
  752. 'style ="style "',
  753. 'style="style"',
  754. ),
  755. array(
  756. 'a',
  757. 'style="style "',
  758. 'style="style"',
  759. ),
  760. array(
  761. 'a',
  762. 'style ="style ="',
  763. '',
  764. ),
  765. array(
  766. 'img',
  767. 'src="mypic.jpg"',
  768. 'src="mypic.jpg"',
  769. ),
  770. array(
  771. 'img',
  772. 'loading="lazy"',
  773. 'loading="lazy"',
  774. ),
  775. array(
  776. 'img',
  777. 'onerror=alert(1)',
  778. '',
  779. ),
  780. array(
  781. 'img',
  782. 'title=>',
  783. 'title="&gt;"',
  784. ),
  785. array(
  786. 'img',
  787. 'title="&garbage";"',
  788. 'title="&amp;garbage&quot;;"',
  789. ),
  790. );
  791. }
  792. /**
  793. * @ticket 34063
  794. */
  795. function test_bdo() {
  796. global $allowedposttags;
  797. $input = '<p>This is <bdo dir="rtl">a BDO tag</bdo>. Weird, <bdo dir="ltr">right?</bdo></p>';
  798. $this->assertSame( $input, wp_kses( $input, $allowedposttags ) );
  799. }
  800. /**
  801. * @ticket 35079
  802. */
  803. function test_ol_reversed() {
  804. global $allowedposttags;
  805. $input = '<ol reversed="reversed"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ol>';
  806. $this->assertSame( $input, wp_kses( $input, $allowedposttags ) );
  807. }
  808. /**
  809. * @ticket 40680
  810. */
  811. function test_wp_kses_attr_no_attributes_allowed_with_empty_array() {
  812. $element = 'foo';
  813. $attribute = 'title="foo" class="bar"';
  814. $this->assertSame( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => array() ), array() ) );
  815. }
  816. /**
  817. * @ticket 40680
  818. */
  819. function test_wp_kses_attr_no_attributes_allowed_with_true() {
  820. $element = 'foo';
  821. $attribute = 'title="foo" class="bar"';
  822. $this->assertSame( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => true ), array() ) );
  823. }
  824. /**
  825. * @ticket 40680
  826. */
  827. function test_wp_kses_attr_single_attribute_is_allowed() {
  828. $element = 'foo';
  829. $attribute = 'title="foo" class="bar"';
  830. $this->assertSame( "<{$element} title=\"foo\">", wp_kses_attr( $element, $attribute, array( 'foo' => array( 'title' => true ) ), array() ) );
  831. }
  832. /**
  833. * @ticket 43312
  834. */
  835. function test_wp_kses_attr_no_attributes_allowed_with_false() {
  836. $element = 'foo';
  837. $attribute = 'title="foo" class="bar"';
  838. $this->assertSame( "<{$element}>", wp_kses_attr( $element, $attribute, array( 'foo' => false ), array() ) );
  839. }
  840. /**
  841. * Testing the safecss_filter_attr() function.
  842. *
  843. * @ticket 37248
  844. * @ticket 42729
  845. * @ticket 48376
  846. * @dataProvider data_test_safecss_filter_attr
  847. *
  848. * @param string $css A string of CSS rules.
  849. * @param string $expected Expected string of CSS rules.
  850. */
  851. public function test_safecss_filter_attr( $css, $expected ) {
  852. $this->assertSame( $expected, safecss_filter_attr( $css ) );
  853. }
  854. /**
  855. * Data Provider for test_safecss_filter_attr().
  856. *
  857. * @return array {
  858. * @type array {
  859. * @string string $css A string of CSS rules.
  860. * @string string $expected Expected string of CSS rules.
  861. * }
  862. * }
  863. */
  864. public function data_test_safecss_filter_attr() {
  865. return array(
  866. // Empty input, empty output.
  867. array(
  868. 'css' => '',
  869. 'expected' => '',
  870. ),
  871. // An arbitrary attribute name isn't allowed.
  872. array(
  873. 'css' => 'foo:bar',
  874. 'expected' => '',
  875. ),
  876. // A single attribute name, with a single value.
  877. array(
  878. 'css' => 'margin-top: 2px',
  879. 'expected' => 'margin-top: 2px',
  880. ),
  881. // Backslash \ isn't supported.
  882. array(
  883. 'css' => 'margin-top: \2px',
  884. 'expected' => '',
  885. ),
  886. // Curly bracket } isn't supported.
  887. array(
  888. 'css' => 'margin-bottom: 2px}',
  889. 'expected' => '',
  890. ),
  891. // A single attribute name, with a single text value.
  892. array(
  893. 'css' => 'text-transform: uppercase',
  894. 'expected' => 'text-transform: uppercase',
  895. ),
  896. // Only lowercase attribute names are supported.
  897. array(
  898. 'css' => 'Text-transform: capitalize',
  899. 'expected' => '',
  900. ),
  901. // Uppercase attribute values goes through.
  902. array(
  903. 'css' => 'text-transform: None',
  904. 'expected' => 'text-transform: None',
  905. ),
  906. // A single attribute, with multiple values.
  907. array(
  908. 'css' => 'font: bold 15px arial, sans-serif',
  909. 'expected' => 'font: bold 15px arial, sans-serif',
  910. ),
  911. // Multiple attributes, with single values.
  912. array(
  913. 'css' => 'font-weight: bold;font-size: 15px',
  914. 'expected' => 'font-weight: bold;font-size: 15px',
  915. ),
  916. // Multiple attributes, separated by a space.
  917. array(
  918. 'css' => 'font-weight: bold; font-size: 15px',
  919. 'expected' => 'font-weight: bold;font-size: 15px',
  920. ),
  921. // Multiple attributes, with multiple values.
  922. array(
  923. 'css' => 'margin: 10px 20px;padding: 5px 10px',
  924. 'expected' => 'margin: 10px 20px;padding: 5px 10px',
  925. ),
  926. // Parenthesis ( is supported for some attributes.
  927. array(
  928. 'css' => 'background: green url("foo.jpg") no-repeat fixed center',
  929. 'expected' => 'background: green url("foo.jpg") no-repeat fixed center',
  930. ),
  931. // Additional background attributes introduced in 5.3.
  932. array(
  933. 'css' => 'background-size: cover;background-size: 200px 100px;background-attachment: local, scroll;background-blend-mode: hard-light',
  934. 'expected' => 'background-size: cover;background-size: 200px 100px;background-attachment: local, scroll;background-blend-mode: hard-light',
  935. ),
  936. // `border-radius` attribute introduced in 5.3.
  937. array(
  938. 'css' => 'border-radius: 10% 30% 50% 70%;border-radius: 30px',
  939. 'expected' => 'border-radius: 10% 30% 50% 70%;border-radius: 30px',
  940. ),
  941. // `flex` and related attributes introduced in 5.3.
  942. array(
  943. 'css' => 'flex: 0 1 auto;flex-basis: 75%;flex-direction: row-reverse;flex-flow: row-reverse nowrap;flex-grow: 2;flex-shrink: 1',
  944. 'expected' => 'flex: 0 1 auto;flex-basis: 75%;flex-direction: row-reverse;flex-flow: row-reverse nowrap;flex-grow: 2;flex-shrink: 1',
  945. ),
  946. // `grid` and related attributes introduced in 5.3.
  947. array(
  948. 'css' => 'grid-template-columns: 1fr 60px;grid-auto-columns: min-content;grid-column-start: span 2;grid-column-end: -1;grid-column-gap: 10%;grid-gap: 10px 20px',
  949. 'expected' => 'grid-template-columns: 1fr 60px;grid-auto-columns: min-content;grid-column-start: span 2;grid-column-end: -1;grid-column-gap: 10%;grid-gap: 10px 20px',
  950. ),
  951. array(
  952. 'css' => 'grid-template-rows: 40px 4em 40px;grid-auto-rows: min-content;grid-row-start: -1;grid-row-end: 3;grid-row-gap: 1em',
  953. 'expected' => 'grid-template-rows: 40px 4em 40px;grid-auto-rows: min-content;grid-row-start: -1;grid-row-end: 3;grid-row-gap: 1em',
  954. ),
  955. // `grid` does not yet support functions or `\`.
  956. array(
  957. 'css' => 'grid-template-columns: repeat(2, 50px 1fr);grid-template: 1em / 20% 20px 1fr',
  958. 'expected' => '',
  959. ),
  960. // `flex` and `grid` alignments introduced in 5.3.
  961. array(
  962. 'css' => 'align-content: space-between;align-items: start;align-self: center;justify-items: center;justify-content: space-between;justify-self: end',
  963. 'expected' => 'align-content: space-between;align-items: start;align-self: center;justify-items: center;justify-content: space-between;justify-self: end',
  964. ),
  965. // `columns` and related attributes introduced in 5.3.
  966. array(
  967. 'css' => 'columns: 6rem auto;column-count: 4;column-fill: balance;column-gap: 9px;column-rule: thick inset blue;column-span: none;column-width: 120px',
  968. 'expected' => 'columns: 6rem auto;column-count: 4;column-fill: balance;column-gap: 9px;column-rule: thick inset blue;column-span: none;column-width: 120px',
  969. ),
  970. // Gradients introduced in 5.3.
  971. array(
  972. 'css' => 'background: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
  973. 'expected' => 'background: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
  974. ),
  975. array(
  976. 'css' => 'background: linear-gradient(135deg,rgba(6,147,227,1) ) (0%,rgb(155,81,224) 100%)',
  977. 'expected' => '',
  978. ),
  979. array(
  980. 'css' => 'background-image: linear-gradient(red,yellow);',
  981. 'expected' => 'background-image: linear-gradient(red,yellow)',
  982. ),
  983. array(
  984. 'css' => 'color: linear-gradient(red,yellow);',
  985. 'expected' => '',
  986. ),
  987. array(
  988. 'css' => 'background-image: linear-gradient(red,yellow); background: prop( red,yellow); width: 100px;',
  989. 'expected' => 'background-image: linear-gradient(red,yellow);width: 100px',
  990. ),
  991. array(
  992. 'css' => 'background: unknown-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
  993. 'expected' => '',
  994. ),
  995. array(
  996. 'css' => 'background: repeating-linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
  997. 'expected' => 'background: repeating-linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
  998. ),
  999. array(
  1000. 'css' => 'width: 100px; height: 100px; background: linear-gradient(135deg,rgba(0,208,132,1) 0%,rgba(6,147,227,1) 100%);',
  1001. 'expected' => 'width: 100px;height: 100px;background: linear-gradient(135deg,rgba(0,208,132,1) 0%,rgba(6,147,227,1) 100%)',
  1002. ),
  1003. array(
  1004. 'css' => 'background: radial-gradient(#ff0, red, yellow, green, rgba(6,147,227,1), rgb(155,81,224) 90%);',
  1005. 'expected' => 'background: radial-gradient(#ff0, red, yellow, green, rgba(6,147,227,1), rgb(155,81,224) 90%)',
  1006. ),
  1007. array(
  1008. 'css' => 'background: radial-gradient(#ff0, red, yellow, green, rgba(6,147,227,1), rgb(155,81,224) 90%);',
  1009. 'expected' => 'background: radial-gradient(#ff0, red, yellow, green, rgba(6,147,227,1), rgb(155,81,224) 90%)',
  1010. ),
  1011. array(
  1012. 'css' => 'background: conic-gradient(at 0% 30%, red 10%, yellow 30%, #1e90ff 50%)',
  1013. 'expected' => 'background: conic-gradient(at 0% 30%, red 10%, yellow 30%, #1e90ff 50%)',
  1014. ),
  1015. // Expressions are not allowed.
  1016. array(
  1017. 'css' => 'height: expression( body.scrollTop + 50 + "px" )',
  1018. 'expected' => '',
  1019. ),
  1020. // RGB color values are not allowed.
  1021. array(
  1022. 'css' => 'color: rgb( 100, 100, 100 )',
  1023. 'expected' => '',
  1024. ),
  1025. // RGBA color values are not allowed.
  1026. array(
  1027. 'css' => 'color: rgb( 100, 100, 100, .4 )',
  1028. 'expected' => '',
  1029. ),
  1030. );
  1031. }
  1032. /**
  1033. * Data attributes are globally accepted.
  1034. *
  1035. * @ticket 33121
  1036. */
  1037. function test_wp_kses_attr_data_attribute_is_allowed() {
  1038. $test = '<div data-foo="foo" data-bar="bar" datainvalid="gone" data--invaild="gone" data-also-invaild-="gone" data-two-hyphens="remains">Pens and pencils</div>';
  1039. $expected = '<div data-foo="foo" data-bar="bar" data-two-hyphens="remains">Pens and pencils</div>';
  1040. $this->assertSame( $expected, wp_kses_post( $test ) );
  1041. }
  1042. /**
  1043. * Ensure wildcard attributes block unprefixed wildcard uses.
  1044. *
  1045. * @ticket 33121
  1046. */
  1047. function test_wildcard_requires_hyphen_after_prefix() {
  1048. $allowed_html = array(
  1049. 'div' => array(
  1050. 'data-*' => true,
  1051. 'on-*' => true,
  1052. ),
  1053. );
  1054. $string = '<div datamelformed-prefix="gone" data="gone" data-="gone" onclick="alert(1)">Malformed attributes</div>';
  1055. $expected = '<div>Malformed attributes</div>';
  1056. $actual = wp_kses( $string, $allowed_html );
  1057. $this->assertSame( $expected, $actual );
  1058. }
  1059. /**
  1060. * Ensure wildcard allows two hyphen.
  1061. *
  1062. * @ticket 33121
  1063. */
  1064. function test_wildcard_allows_two_hyphens() {
  1065. $allowed_html = array(
  1066. 'div' => array(
  1067. 'data-*' => true,
  1068. ),
  1069. );
  1070. $string = '<div data-wp-id="pens-and-pencils">Well formed attribute</div>';
  1071. $expected = '<div data-wp-id="pens-and-pencils">Well formed attribute</div>';
  1072. $actual = wp_kses( $string, $allowed_html );
  1073. $this->assertSame( $expected, $actual );
  1074. }
  1075. /**
  1076. * Ensure wildcard attributes only support valid prefixes.
  1077. *
  1078. * @dataProvider data_wildcard_attribute_prefixes
  1079. *
  1080. * @ticket 33121
  1081. */
  1082. function test_wildcard_attribute_prefixes( $wildcard_attribute, $expected ) {
  1083. $allowed_html = array(
  1084. 'div' => array(
  1085. $wildcard_attribute => true,
  1086. ),
  1087. );
  1088. $name = str_replace( '*', strtolower( __FUNCTION__ ), $wildcard_attribute );
  1089. $value = __FUNCTION__;
  1090. $whole = "{$name}=\"{$value}\"";
  1091. $actual = wp_kses_attr_check( $name, $value, $whole, 'n', 'div', $allowed_html );
  1092. $this->assertSame( $expected, $actual );
  1093. }
  1094. /**
  1095. * @return array Array of arguments for wildcard testing
  1096. * [0] The prefix being tested.
  1097. * [1] The outcome of `wp_kses_attr_check` for the prefix.
  1098. */
  1099. function data_wildcard_attribute_prefixes() {
  1100. return array(
  1101. // Ends correctly.
  1102. array( 'data-*', true ),
  1103. // Does not end with trialing `-`.
  1104. array( 'data*', false ),
  1105. // Multiple wildcards.
  1106. array( 'd*ta-*', false ),
  1107. array( 'data**', false ),
  1108. );
  1109. }
  1110. /**
  1111. * Test URL sanitization in the style tag.
  1112. *
  1113. * @dataProvider data_kses_style_attr_with_url
  1114. *
  1115. * @ticket 45067
  1116. *
  1117. * @param $input string The style attribute saved in the editor.
  1118. * @param $expected string The sanitized style attribute.
  1119. */
  1120. function test_kses_style_attr_with_url( $input, $expected ) {
  1121. $actual = safecss_filter_attr( $input );
  1122. $this->assertSame( $expected, $actual );
  1123. }
  1124. /**
  1125. * Data provider testing style attribute sanitization.
  1126. *
  1127. * @return array Nested array of input, expected pairs.
  1128. */
  1129. function data_kses_style_attr_with_url() {
  1130. return array(
  1131. /*
  1132. * Valid use cases.
  1133. */
  1134. // Double quotes.
  1135. array(
  1136. 'background-image: url( "http://example.com/valid.gif" );',
  1137. 'background-image: url( "http://example.com/valid.gif" )',
  1138. ),
  1139. // Single quotes.
  1140. array(
  1141. "background-image: url( 'http://example.com/valid.gif' );",
  1142. "background-image: url( 'http://example.com/valid.gif' )",
  1143. ),
  1144. // No quotes.
  1145. array(
  1146. 'background-image: url( http://example.com/valid.gif );',
  1147. 'background-image: url( http://example.com/valid.gif )',
  1148. ),
  1149. // Single quotes, extra spaces.
  1150. array(
  1151. "background-image: url( ' http://example.com/valid.gif ' );",
  1152. "background-image: url( ' http://example.com/valid.gif ' )",
  1153. ),
  1154. // Line breaks, single quotes.
  1155. array(
  1156. "background-image: url(\n'http://example.com/valid.gif' );",
  1157. "background-image: url('http://example.com/valid.gif' )",
  1158. ),
  1159. // Tabs not spaces, single quotes.
  1160. array(
  1161. "background-image: url(\t'http://example.com/valid.gif'\t\t);",
  1162. "background-image: url('http://example.com/valid.gif')",
  1163. ),
  1164. // Single quotes, absolute path.
  1165. array(
  1166. "background: url('/valid.gif');",
  1167. "background: url('/valid.gif')",
  1168. ),
  1169. // Single quotes, relative path.
  1170. array(
  1171. "background: url('../wp-content/uploads/2018/10/valid.gif');",
  1172. "background: url('../wp-content/uploads/2018/10/valid.gif')",
  1173. ),
  1174. // Error check: valid property not containing a URL.
  1175. array(
  1176. 'background: red',
  1177. 'background: red',
  1178. ),
  1179. /*
  1180. * Invalid use cases.
  1181. */
  1182. // Attribute doesn't support URL properties.
  1183. array(
  1184. 'color: url( "http://example.com/invalid.gif" );',
  1185. '',
  1186. ),
  1187. // Mismatched quotes.
  1188. array(
  1189. 'background-image: url( "http://example.com/valid.gif\' );',
  1190. '',
  1191. ),
  1192. // Bad protocol, double quotes.
  1193. array(
  1194. 'background-image: url( "bad://example.com/invalid.gif" );',
  1195. '',
  1196. ),
  1197. // Bad protocol, single quotes.
  1198. array(
  1199. "background-image: url( 'bad://example.com/invalid.gif' );",
  1200. '',
  1201. ),
  1202. // Bad protocol, single quotes.
  1203. array(
  1204. "background-image: url( 'bad://example.com/invalid.gif' );",
  1205. '',
  1206. ),
  1207. // Bad protocol, single quotes, strange spacing.
  1208. array(
  1209. "background-image: url( ' \tbad://example.com/invalid.gif ' );",
  1210. '',
  1211. ),
  1212. // Bad protocol, no quotes.
  1213. array(
  1214. 'background-image: url( bad://example.com/invalid.gif );',
  1215. '',
  1216. ),
  1217. // No URL inside url().
  1218. array(
  1219. 'background-image: url();',
  1220. '',
  1221. ),
  1222. // Malformed, no closing `)`.
  1223. array(
  1224. 'background-image: url( "http://example.com" ;',
  1225. '',
  1226. ),
  1227. // Malformed, no closing `"`.
  1228. array(
  1229. 'background-image: url( "http://example.com );',
  1230. '',
  1231. ),
  1232. );
  1233. }
  1234. /**
  1235. * Testing the safecss_filter_attr() function with the safecss_filter_attr_allow_css filter.
  1236. *
  1237. * @ticket 37134
  1238. *
  1239. * @dataProvider data_test_safecss_filter_attr_filtered
  1240. *
  1241. * @param string $css A string of CSS rules.
  1242. * @param string $expected Expected string of CSS rules.
  1243. */
  1244. public function test_safecss_filter_attr_filtered( $css, $expected ) {
  1245. add_filter( 'safecss_filter_attr_allow_css', '__return_true' );
  1246. $this->assertSame( $expected, safecss_filter_attr( $css ) );
  1247. remove_filter( 'safecss_filter_attr_allow_css', '__return_true' );
  1248. }
  1249. /**
  1250. * Data Provider for test_safecss_filter_attr_filtered().
  1251. *
  1252. * @return array {
  1253. * @type array {
  1254. * @string string $css A string of CSS rules.
  1255. * @string string $expected Expected string of CSS rules.
  1256. * }
  1257. * }
  1258. */
  1259. public function data_test_safecss_filter_attr_filtered() {
  1260. return array(
  1261. // A single attribute name, with a single value.
  1262. array(
  1263. 'css' => 'margin-top: 2px',
  1264. 'expected' => 'margin-top: 2px',
  1265. ),
  1266. // Backslash \ can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1267. array(
  1268. 'css' => 'margin-top: \2px',
  1269. 'expected' => 'margin-top: \2px',
  1270. ),
  1271. // Curly bracket } can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1272. array(
  1273. 'css' => 'margin-bottom: 2px}',
  1274. 'expected' => 'margin-bottom: 2px}',
  1275. ),
  1276. // Parenthesis ) can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1277. array(
  1278. 'css' => 'margin-bottom: 2px)',
  1279. 'expected' => 'margin-bottom: 2px)',
  1280. ),
  1281. // Ampersand & can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1282. array(
  1283. 'css' => 'margin-bottom: 2px&',
  1284. 'expected' => 'margin-bottom: 2px&',
  1285. ),
  1286. // Expressions can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1287. array(
  1288. 'css' => 'height: expression( body.scrollTop + 50 + "px" )',
  1289. 'expected' => 'height: expression( body.scrollTop + 50 + "px" )',
  1290. ),
  1291. // RGB color values can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1292. array(
  1293. 'css' => 'color: rgb( 100, 100, 100 )',
  1294. 'expected' => 'color: rgb( 100, 100, 100 )',
  1295. ),
  1296. // RGBA color values can be allowed with the 'safecss_filter_attr_allow_css' filter.
  1297. array(
  1298. 'css' => 'color: rgb( 100, 100, 100, .4 )',
  1299. 'expected' => 'color: rgb( 100, 100, 100, .4 )',
  1300. ),
  1301. );
  1302. }
  1303. /**
  1304. * Test filtering a standard img tag.
  1305. *
  1306. * @ticket 50731
  1307. */
  1308. function test_wp_kses_img_tag_standard_attributes() {
  1309. $html = array(
  1310. '<img',
  1311. 'loading="lazy"',
  1312. 'src="https://example.com/img.jpg"',
  1313. 'width="1000"',
  1314. 'height="1000"',
  1315. 'alt=""',
  1316. 'class="wp-image-1000"',
  1317. '/>',
  1318. );
  1319. $html = implode( ' ', $html );
  1320. $this->assertSame( $html, wp_kses_post( $html ) );
  1321. }
  1322. }