@@ -374,6 +374,31 @@ public function filter_update_custom_css_data( $data, $args ) {
374374 return $ data ;
375375 }
376376
377+ /**
378+ * Ensure that dangerous STYLE tag contents do not break HTML output.
379+ *
380+ * @ticket 64418
381+ * @covers ::wp_update_custom_css_post
382+ * @covers ::wp_custom_css_cb
383+ */
384+ public function test_wp_custom_css_cb_escapes_dangerous_html () {
385+ wp_update_custom_css_post (
386+ '*::before { content: "</style><script>alert(1)</script>"; } ' ,
387+ array (
388+ 'stylesheet ' => $ this ->setting ->stylesheet ,
389+ )
390+ );
391+ $ output = get_echo ( 'wp_custom_css_cb ' );
392+ $ expected =
393+ <<<'HTML'
394+ <style id="wp-custom-css">
395+ *::before { content: "\3c\2fstyle><script>alert(1)</script>"; }
396+ </style>
397+
398+ HTML;
399+ $ this ->assertEqualHTML ( $ expected , $ output );
400+ }
401+
377402 /**
378403 * Tests that validation errors are caught appropriately.
379404 *
@@ -382,8 +407,7 @@ public function filter_update_custom_css_data( $data, $args ) {
382407 *
383408 * @covers WP_Customize_Custom_CSS_Setting::validate
384409 */
385- public function test_validate () {
386-
410+ public function test_validate_basic_css () {
387411 // Empty CSS throws no errors.
388412 $ result = $ this ->setting ->validate ( '' );
389413 $ this ->assertTrue ( $ result );
@@ -393,9 +417,84 @@ public function test_validate() {
393417 $ result = $ this ->setting ->validate ( $ basic_css );
394418 $ this ->assertTrue ( $ result );
395419
396- // Check for markup .
420+ // Check for illegal closing STYLE tag .
397421 $ unclosed_comment = $ basic_css . '</style> ' ;
398422 $ result = $ this ->setting ->validate ( $ unclosed_comment );
399423 $ this ->assertArrayHasKey ( 'illegal_markup ' , $ result ->errors );
400424 }
425+
426+ /**
427+ * @ticket 64418
428+ * @covers WP_Customize_Custom_CSS_Setting::validate
429+ */
430+ public function test_validate_accepts_css_property_at_rule () {
431+ $ css =
432+ <<<'CSS'
433+ @property --animate {
434+ syntax: "<custom-ident>";
435+ inherits: true;
436+ initial-value: false;
437+ }
438+ CSS;
439+ $ this ->assertTrue ( $ this ->setting ->validate ( $ css ) );
440+ }
441+
442+ /**
443+ * @ticket 64418
444+ * @covers ::wp_update_custom_css_post
445+ * @covers ::wp_custom_css_cb
446+ */
447+ public function test_save_and_print_property_at_rule () {
448+ $ css =
449+ <<<'CSS'
450+ @property --animate {
451+ syntax: "<custom-ident>";
452+ inherits: true;
453+ initial-value: false;
454+ }
455+ CSS;
456+ wp_update_custom_css_post ( $ css , array ( 'stylesheet ' => $ this ->setting ->stylesheet ) );
457+ $ output = get_echo ( 'wp_custom_css_cb ' );
458+ $ expected = "<style id='wp-custom-css'> \n{$ css }\n</style> \n" ;
459+ $ this ->assertEqualHTML ( $ expected , $ output );
460+ }
461+
462+ /**
463+ * @dataProvider data_custom_css_disallowed
464+ *
465+ * @ticket 64418
466+ * @covers WP_Customize_Custom_CSS_Setting::validate
467+ */
468+ public function test_validate_prevents ( $ css , $ expected_error_message ) {
469+ $ result = $ this ->setting ->validate ( $ css );
470+ $ this ->assertWPError ( $ result );
471+ $ this ->assertSame ( $ expected_error_message , $ result ->get_error_message () );
472+ }
473+
474+ /**
475+ * Data provider.
476+ *
477+ * @return array<string, string[]>
478+ */
479+ public static function data_custom_css_disallowed (): array {
480+ return array (
481+ 'style close tag ' => array ( 'css…</style>…css ' , 'The CSS must not contain "</style>". ' ),
482+ 'style close tag upper case ' => array ( '</STYLE> ' , 'The CSS must not contain "</STYLE>". ' ),
483+ 'style close tag mixed case ' => array ( '</sTyLe> ' , 'The CSS must not contain "</sTyLe>". ' ),
484+ 'style close tag in comment ' => array ( '/*</style>*/ ' , 'The CSS must not contain "</style>". ' ),
485+ 'style close tag (/) ' => array ( '</style/ ' , 'The CSS must not contain "</style/". ' ),
486+ 'style close tag (\t) ' => array ( "</style \t" , "The CSS must not contain \"</style \t\". " ),
487+ 'style close tag (\f) ' => array ( "</style \f" , "The CSS must not contain \"</style \f\". " ),
488+ 'style close tag (\r) ' => array ( "</style \r" , "The CSS must not contain \"</style \r\". " ),
489+ 'style close tag (\n) ' => array ( "</style \n" , "The CSS must not contain \"</style \n\". " ),
490+ 'style close tag (" ") ' => array ( '</style ' , 'The CSS must not contain "</style ". ' ),
491+ 'truncated "<" ' => array ( '< ' , 'The CSS must not end in "<". ' ),
492+ 'truncated "</" ' => array ( '</ ' , 'The CSS must not end in "</". ' ),
493+ 'truncated "</s" ' => array ( '</s ' , 'The CSS must not end in "</s". ' ),
494+ 'truncated "</ST" ' => array ( '</ST ' , 'The CSS must not end in "</ST". ' ),
495+ 'truncated "</sty" ' => array ( '</sty ' , 'The CSS must not end in "</sty". ' ),
496+ 'truncated "</STYL" ' => array ( '</STYL ' , 'The CSS must not end in "</STYL". ' ),
497+ 'truncated "</stYle" ' => array ( '</stYle ' , 'The CSS must not end in "</stYle". ' ),
498+ );
499+ }
401500}
0 commit comments