From 7c0102082ccb677bf6ab1abcfa0876829fc0a85a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 11:54:26 +0100 Subject: [PATCH 01/39] Relax style tag contents validation STYLE tags may contain any raw text up to a closing STYLE tag https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm --- .../class-wp-customize-custom-css-setting.php | 10 +++++++++- .../class-wp-rest-global-styles-controller.php | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index aab0e475304ea..da8e83ebc7cc6 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -163,7 +163,15 @@ public function validate( $value ) { $validity = new WP_Error(); - if ( preg_match( '#]#', $css ) ) { $validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 908ebe4bcc777..bc2e52a82d75b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -668,7 +668,15 @@ public function get_theme_items( $request ) { * @return true|WP_Error True if the input was validated, otherwise WP_Error. */ protected function validate_custom_css( $css ) { - if ( preg_match( '#]#', $css ) ) { return new WP_Error( 'rest_custom_css_illegal_markup', __( 'Markup is not allowed in CSS.' ), From 511284e1d7565983167a693ff10bc87dd0f70ba5 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:33:39 +0100 Subject: [PATCH 02/39] Apply suggestions from code review Strictly speaking, a `STYLE` tag will be closed by a `STYLE` close tag. However, the CSS contents are concatenated together and the strict check for a STYLE close tag may later be invalidated by concatenation. --- .../customize/class-wp-customize-custom-css-setting.php | 2 +- .../endpoints/class-wp-rest-global-styles-controller.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index da8e83ebc7cc6..0f49278c39759 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -171,7 +171,7 @@ public function validate( $value ) { * * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm */ - if ( preg_match( '#]#', $css ) ) { + if ( stripos( $css, 'add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index bc2e52a82d75b..36049b1132915 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -676,7 +676,7 @@ protected function validate_custom_css( $css ) { * * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm */ - if ( preg_match( '#]#', $css ) ) { + if ( stripos( $css, ' Date: Wed, 17 Dec 2025 12:00:53 +0100 Subject: [PATCH 03/39] Update comments and document in since annotation --- .../customize/class-wp-customize-custom-css-setting.php | 4 ++-- .../endpoints/class-wp-rest-global-styles-controller.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index 0f49278c39759..908fd7da32ea4 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -145,14 +145,14 @@ public function value() { } /** - * Validate a received value for being valid CSS. + * Validate a received value for being safe HTML STYLE tag contents. * - * Checks for imbalanced braces, brackets, and comments. * Notifications are rendered when the customizer state is saved. * * @since 4.7.0 * @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor. * @since 5.9.0 Renamed `$css` to `$value` for PHP 8 named parameter support. + * @since 7.0.0 Relaxed to only check for safe HTML STYLE tag contents. * * @param string $value CSS to validate. * @return true|WP_Error True if the input was validated, otherwise WP_Error. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 36049b1132915..3ee77008e344e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -659,10 +659,11 @@ public function get_theme_items( $request ) { /** * Validate style.css as valid CSS. * - * Currently just checks for invalid markup. + * Currently just checks that CSS will not break an HTML STYLE tag. * * @since 6.2.0 * @since 6.4.0 Changed method visibility to protected. + * @since 7.0.0 Relaxed to only check for safe HTML STYLE tag contents. * * @param string $css CSS to validate. * @return true|WP_Error True if the input was validated, otherwise WP_Error. From db273b765e7190413937499b7529e06ef14bd51a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:01:07 +0100 Subject: [PATCH 04/39] Update customizer error message --- .../customize/class-wp-customize-custom-css-setting.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index 908fd7da32ea4..bf2cffdbb8ccf 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -172,7 +172,7 @@ public function validate( $value ) { * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm */ if ( stripos( $css, 'add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) ); + $validity->add( 'illegal_markup', __( 'CSS must not contain possible closing STYLE tag "has_errors() ) { From 62d59c221442eb329becd1dd617c73e61ca568b3 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:45:32 +0100 Subject: [PATCH 05/39] Update wp_custom_css_cb to rely on HTML API for safe SCRIPT tag printing. --- src/wp-includes/theme.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 558bea6ae9e02..b9c4945fa5e6a 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1967,17 +1967,18 @@ function _custom_background_cb() { */ function wp_custom_css_cb() { $styles = wp_get_custom_css(); - if ( $styles || is_customize_preview() ) : - $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; - ?> - id="wp-custom-css"> - - - ' ); + $processor->next_tag(); + if ( ! current_theme_supports( 'html5', 'style' ) ) { + $processor->set_attribute( 'type', 'text/css' ); + } + $processor->set_attribute( 'id', 'wp-custom-css' ); + $processor->set_modifiable_text( $styles ); + echo $processor->get_updated_html(); } /** From f3a1716d15eb6d5931eeb7fe891ec5f8d8afd8f7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:48:39 +0100 Subject: [PATCH 06/39] Wrap customizer CSS test in newlines --- src/wp-includes/theme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index b9c4945fa5e6a..452b2588ebe9f 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1977,7 +1977,7 @@ function wp_custom_css_cb() { $processor->set_attribute( 'type', 'text/css' ); } $processor->set_attribute( 'id', 'wp-custom-css' ); - $processor->set_modifiable_text( $styles ); + $processor->set_modifiable_text( "\n{$styles}\n" ); echo $processor->get_updated_html(); } From e793caa5d4fd8dd67dfe4dbdb33b9bce3196ef7f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:55:21 +0100 Subject: [PATCH 07/39] Use HTML API for style tags in script-loader --- src/wp-includes/script-loader.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 56986c3d80a79..e968ecb693758 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2417,10 +2417,12 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - echo "\n"; - echo $wp_styles->print_code; - echo sprintf( "\n/*# sourceURL=%s */", rawurlencode( $concat_source_url ) ); - echo "\n\n"; + $processor = new WP_HTML_Tag_Processor( "" ); + $processor->next_tag(); + $style_tag_contents = "\n{$wp_styles->print_code}\n" + . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); + $processor->set_modifiable_text( $style_tag_contents ); + echo $processor->get_updated_html(); } } @@ -3217,7 +3219,10 @@ function wp_enqueue_block_support_styles( $style, $priority = 10 ) { add_action( $action_hook_name, static function () use ( $style ) { - echo "\n"; + $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor->next_tag(); + $processor->set_modifiable_text( $style ); + echo $processor->get_updated_html(); }, $priority ); From aa7852c79b079fbd892c29b3dc77ccf358750d88 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 13:06:48 +0100 Subject: [PATCH 08/39] Use HTML Tag Processor to produce WP_Styles style tags --- src/wp-includes/class-wp-styles.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 2e037f128809f..7030d20ee00b9 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -178,12 +178,11 @@ public function do_item( $handle, $group = false ) { $inline_style = $this->print_inline_style( $handle, false ); if ( $inline_style ) { - $inline_style_tag = sprintf( - "\n", - esc_attr( $handle ), - $this->type_attr, - $inline_style - ); + $processor = new WP_HTML_Tag_Processor( "type_attr}>\n" ); + $processor->next_tag(); + $processor->set_attribute( 'id', "{$handle}-inline-css" ); + $processor->set_modifiable_text( "\n{$inline_style}\n" ); + $inline_style_tag = $processor->get_updated_html(); } else { $inline_style_tag = ''; } @@ -359,12 +358,11 @@ public function print_inline_style( $handle, $display = true ) { return $output; } - printf( - "\n", - esc_attr( $handle ), - $this->type_attr, - $output - ); + $processor = new WP_HTML_Tag_Processor( "type_attr}>\n" ); + $processor->next_tag(); + $processor->set_attribute( 'id', "{$handle}-inline-css" ); + $processor->set_modifiable_text( "\n{$output}\n" ); + echo $processor->get_updated_html(); return true; } From 93cddc34426a2745fc30bc7366c0b22c19690e8b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 13:15:37 +0100 Subject: [PATCH 09/39] Use HTML Tag Processor for STYLE tags in theme.php --- src/wp-includes/theme.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 452b2588ebe9f..b8d35a255d35c 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1953,11 +1953,13 @@ function _custom_background_cb() { $style .= $image . $position . $size . $repeat . $attachment; } - ?> - id="custom-background-css"> -body.custom-background { } - - " ); + $processor->next_tag(); + + $style_tag_content = 'body.custom-background { ' . trim( $style ) . ' }'; + $processor->set_modifiable_text( "\n{$style_tag_content}\n" ); + echo $processor->get_updated_html(); } /** @@ -1967,7 +1969,7 @@ function _custom_background_cb() { */ function wp_custom_css_cb() { $styles = wp_get_custom_css(); - if ( ! $styles || ! is_customize_preview() ) { + if ( ! $styles && ! is_customize_preview() ) { return; } From 41a1fe3372a28ca3b830324e104436cb9bfd97ae Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 13:47:42 +0100 Subject: [PATCH 10/39] PICKME: Update styles tests to use assertEqualHTML --- tests/phpunit/tests/dependencies/styles.php | 24 ++++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index 74e4db47330b4..27e37391e2f7b 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -68,7 +68,7 @@ public function test_wp_enqueue_style() { $expected .= "\n"; $expected .= "\n"; - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); // No styles left to print. $this->assertSame( '', get_echo( 'wp_print_styles' ) ); @@ -88,7 +88,7 @@ public function test_wp_enqueue_style_with_html5_support_does_not_contain_type_a $ver = get_bloginfo( 'version' ); $expected = "\n"; - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** @@ -103,7 +103,7 @@ public function test_awkward_handles_are_supported_consistently( $handle ) { $expected = "\n"; - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** @@ -157,7 +157,7 @@ public function test_protocols() { $expected .= "\n"; // Go! - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); // No styles left to print. $this->assertSame( '', get_echo( 'wp_print_styles' ) ); @@ -186,8 +186,7 @@ public function test_inline_styles() { wp_enqueue_style( 'handle', 'http://example.com', array(), 1 ); wp_add_inline_style( 'handle', $style ); - // No styles left to print. - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** @@ -215,7 +214,7 @@ public function test_inline_styles_concat() { wp_add_inline_style( 'handle', $style ); wp_print_styles(); - $this->assertSame( $expected, $wp_styles->print_html ); + $this->assertEqualHTML( $expected, $wp_styles->print_html ); } /** @@ -233,7 +232,7 @@ public function test_inline_styles_concat() { * @param string $expected Expected result. */ public function test_normalize_relative_css_links( $css, $expected ) { - $this->assertSame( + $this->assertEqualHTML( $expected, _wp_normalize_relative_css_links( $css, site_url( 'wp-content/themes/test/style.css' ) ) ); @@ -311,8 +310,7 @@ public function test_multiple_inline_styles() { wp_add_inline_style( 'handle', $style1 ); wp_add_inline_style( 'handle', $style2 ); - // No styles left to print. - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** @@ -337,7 +335,7 @@ public function test_plugin_doing_inline_styles_wrong() { wp_add_inline_style( 'handle', "" ); - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** @@ -351,7 +349,7 @@ public function test_unnecessary_style_tags() { wp_enqueue_style( 'handle', 'http://example.com', array(), 1 ); - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** @@ -399,7 +397,7 @@ public function test_wp_add_inline_style_for_handle_without_source() { wp_enqueue_style( 'handle-three' ); wp_add_inline_style( 'handle-three', $style ); - $this->assertSame( $expected, get_echo( 'wp_print_styles' ) ); + $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) ); } /** From 99b8733d7da31fd2c4a318e6868bc11df7479d93 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 13:49:28 +0100 Subject: [PATCH 11/39] Simplify conditional style no-print test --- tests/phpunit/tests/dependencies/styles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index 27e37391e2f7b..ee33f4d4bf1b3 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -359,12 +359,12 @@ public function test_unnecessary_style_tags() { * @expectedDeprecated WP_Dependencies->add_data() */ public function test_conditional_inline_styles_are_also_conditional() { - $expected = ''; wp_enqueue_style( 'handle', 'http://example.com', array(), 1 ); wp_style_add_data( 'handle', 'conditional', 'IE' ); wp_add_inline_style( 'handle', 'a { color: blue; }' ); - $this->assertSameIgnoreEOL( $expected, get_echo( 'wp_print_styles' ) ); + // Conditional styles are disabled. + $this->assertSame( '', get_echo( 'wp_print_styles' ) ); } /** From 2ea6a12b257b415e8fe9d44f513d32257db2e8c7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 18 Dec 2025 12:23:38 +0100 Subject: [PATCH 12/39] Correctly check presence with stripos Co-authored-by: Weston Ruter --- .../customize/class-wp-customize-custom-css-setting.php | 2 +- .../endpoints/class-wp-rest-global-styles-controller.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index bf2cffdbb8ccf..ebe61fbf270f6 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -171,7 +171,7 @@ public function validate( $value ) { * * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm */ - if ( stripos( $css, 'add( 'illegal_markup', __( 'CSS must not contain possible closing STYLE tag " Date: Mon, 22 Dec 2025 19:50:21 +0100 Subject: [PATCH 13/39] Update failing test, add test for allowed CSS --- .../rest-global-styles-controller.php | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index 59986e597c71b..ab4395db02c2f 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -650,6 +650,7 @@ public function test_update_item_valid_styles_css() { /** * @covers WP_REST_Global_Styles_Controller::update_item * @ticket 57536 + * @ticket 64418 */ public function test_update_item_invalid_styles_css() { wp_set_current_user( self::$admin_id ); @@ -659,7 +660,9 @@ public function test_update_item_invalid_styles_css() { $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); $request->set_body_params( array( - 'styles' => array( 'css' => '

test

body { color: red; }' ), + 'styles' => array( + 'css' => 'dispatch( $request ); @@ -826,4 +829,32 @@ public function test_global_styles_route_args_schema() { $this->assertArrayHasKey( 'type', $route_data[0]['args']['id'] ); $this->assertSame( 'integer', $route_data[0]['args']['id']['type'] ); } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 64418 + */ + public function test_update_allows_valid_css_with_more_syntax() { + wp_set_current_user( self::$admin_id ); + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $css = <<<'CSS' +@property --animate { + syntax: ""; + inherits: true; + initial-value: false; +} +CSS; + $request->set_body_params( + array( + 'styles' => array( 'css' => $css ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( $css, $data['styles']['css'] ); + } } From c124eb5774a2bbaaa1f913a9d8804101e5e9cf1b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:45:32 +0100 Subject: [PATCH 14/39] Update wp_custom_css_cb to rely on HTML API for safe SCRIPT tag printing. --- src/wp-includes/theme.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 558bea6ae9e02..b9c4945fa5e6a 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1967,17 +1967,18 @@ function _custom_background_cb() { */ function wp_custom_css_cb() { $styles = wp_get_custom_css(); - if ( $styles || is_customize_preview() ) : - $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; - ?> - id="wp-custom-css"> - - - ' ); + $processor->next_tag(); + if ( ! current_theme_supports( 'html5', 'style' ) ) { + $processor->set_attribute( 'type', 'text/css' ); + } + $processor->set_attribute( 'id', 'wp-custom-css' ); + $processor->set_modifiable_text( $styles ); + echo $processor->get_updated_html(); } /** From e05515657939360a76195b3c66c0a7cb83e56ff4 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:48:39 +0100 Subject: [PATCH 15/39] Wrap customizer CSS test in newlines --- src/wp-includes/theme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index b9c4945fa5e6a..452b2588ebe9f 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1977,7 +1977,7 @@ function wp_custom_css_cb() { $processor->set_attribute( 'type', 'text/css' ); } $processor->set_attribute( 'id', 'wp-custom-css' ); - $processor->set_modifiable_text( $styles ); + $processor->set_modifiable_text( "\n{$styles}\n" ); echo $processor->get_updated_html(); } From 33f96169b9dcc21cb9cc919ffc32f2132a5d053f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 12:55:21 +0100 Subject: [PATCH 16/39] Use HTML API for style tags in script-loader --- src/wp-includes/script-loader.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 56986c3d80a79..e968ecb693758 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2417,10 +2417,12 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - echo "\n"; - echo $wp_styles->print_code; - echo sprintf( "\n/*# sourceURL=%s */", rawurlencode( $concat_source_url ) ); - echo "\n\n"; + $processor = new WP_HTML_Tag_Processor( "" ); + $processor->next_tag(); + $style_tag_contents = "\n{$wp_styles->print_code}\n" + . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); + $processor->set_modifiable_text( $style_tag_contents ); + echo $processor->get_updated_html(); } } @@ -3217,7 +3219,10 @@ function wp_enqueue_block_support_styles( $style, $priority = 10 ) { add_action( $action_hook_name, static function () use ( $style ) { - echo "\n"; + $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor->next_tag(); + $processor->set_modifiable_text( $style ); + echo $processor->get_updated_html(); }, $priority ); From 606539ea18065a60b3181752705c7376c6a29e9a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 13:06:48 +0100 Subject: [PATCH 17/39] Use HTML Tag Processor to produce WP_Styles style tags --- src/wp-includes/class-wp-styles.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 9b210b2df9d30..6d47bc15d56fd 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -183,12 +183,11 @@ public function do_item( $handle, $group = false ) { $inline_style = $this->print_inline_style( $handle, false ); if ( $inline_style ) { - $inline_style_tag = sprintf( - "\n", - esc_attr( $handle ), - $this->type_attr, - $inline_style - ); + $processor = new WP_HTML_Tag_Processor( "type_attr}>\n" ); + $processor->next_tag(); + $processor->set_attribute( 'id', "{$handle}-inline-css" ); + $processor->set_modifiable_text( "\n{$inline_style}\n" ); + $inline_style_tag = $processor->get_updated_html(); } else { $inline_style_tag = ''; } @@ -364,12 +363,11 @@ public function print_inline_style( $handle, $display = true ) { return $output; } - printf( - "\n", - esc_attr( $handle ), - $this->type_attr, - $output - ); + $processor = new WP_HTML_Tag_Processor( "type_attr}>\n" ); + $processor->next_tag(); + $processor->set_attribute( 'id', "{$handle}-inline-css" ); + $processor->set_modifiable_text( "\n{$output}\n" ); + echo $processor->get_updated_html(); return true; } From c938d4c8a7e0719a879d65d273e077dbcff6343f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 17 Dec 2025 13:15:37 +0100 Subject: [PATCH 18/39] Use HTML Tag Processor for STYLE tags in theme.php --- src/wp-includes/theme.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 452b2588ebe9f..b8d35a255d35c 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1953,11 +1953,13 @@ function _custom_background_cb() { $style .= $image . $position . $size . $repeat . $attachment; } - ?> - id="custom-background-css"> -body.custom-background { } - - " ); + $processor->next_tag(); + + $style_tag_content = 'body.custom-background { ' . trim( $style ) . ' }'; + $processor->set_modifiable_text( "\n{$style_tag_content}\n" ); + echo $processor->get_updated_html(); } /** @@ -1967,7 +1969,7 @@ function _custom_background_cb() { */ function wp_custom_css_cb() { $styles = wp_get_custom_css(); - if ( ! $styles || ! is_customize_preview() ) { + if ( ! $styles && ! is_customize_preview() ) { return; } From dd919f13c7ce2322a90d6f70a587518a32147e00 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 23 Dec 2025 13:40:23 +0100 Subject: [PATCH 19/39] Build font style tags with HTML API --- src/wp-includes/fonts/class-wp-font-face.php | 36 ++++---------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 07cd3d6de9002..fd66825a0b4e5 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -118,7 +118,13 @@ public function generate_and_print( array $fonts ) { return; } - printf( $this->get_style_element(), $css ); + $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor->next_tag(); + foreach ( $this->style_tag_attrs as $name => $value ) { + $processor->set_attribute( $name, $value ); + } + $processor->set_modifiable_text( "\n{$css}\n" ); + echo $processor->get_updated_html(); } /** @@ -219,34 +225,6 @@ private function validate_font_face_declarations( array $font_face ) { return $font_face; } - /** - * Gets the style element for wrapping the `@font-face` CSS. - * - * @since 6.4.0 - * - * @return string The style element. - */ - private function get_style_element() { - $attributes = $this->generate_style_element_attributes(); - - return "\n"; - } - - /** - * Gets the defined \n"; $expected_output = sprintf( $style_element, $expected ); - $this->expectOutputString( $expected_output ); - $font_face->generate_and_print( $fonts ); + $output = get_echo( + function () use ( $font_face, $fonts ) { + $font_face->generate_and_print( $fonts ); + } + ); + $this->assertEqualHTML( $expected_output, $output ); } } diff --git a/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php b/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php index 2fa64559c2049..1d6ef663c5f22 100644 --- a/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php +++ b/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php @@ -37,8 +37,12 @@ public function test_should_not_print_when_no_fonts() { public function test_should_print_given_fonts( array $fonts, $expected ) { $expected_output = $this->get_expected_styles_output( $expected ); - $this->expectOutputString( $expected_output ); - wp_print_font_faces( $fonts ); + $output = get_echo( + function () use ( $fonts ) { + wp_print_font_faces( $fonts ); + } + ); + $this->assertEqualHTML( $expected_output, $output ); } public function test_should_escape_tags() { @@ -60,9 +64,13 @@ public function test_should_escape_tags() { CSS; - $this->expectOutputString( $expected_output ); - wp_print_font_faces( $fonts ); + $output = get_echo( + function () use ( $fonts ) { + wp_print_font_faces( $fonts ); + } + ); + $this->assertEqualHTML( $expected_output, $output ); } public function test_should_print_fonts_in_merged_data() { @@ -71,8 +79,8 @@ public function test_should_print_fonts_in_merged_data() { $expected = $this->get_expected_fonts_for_fonts_block_theme( 'font_face_styles' ); $expected_output = $this->get_expected_styles_output( $expected ); - $this->expectOutputString( $expected_output ); - wp_print_font_faces(); + $output = get_echo( 'wp_print_font_faces' ); + $this->assertEqualHTML( $expected_output, $output ); } private function get_expected_styles_output( $styles ) { diff --git a/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php b/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php index a59ba882a4e86..5dd6304fd2f7b 100644 --- a/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php +++ b/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php @@ -43,8 +43,8 @@ public function test_should_print_fonts_in_style_variations() { $expected = $this->get_custom_style_variations( 'expected_styles' ); $expected_output = $this->get_expected_styles_output( $expected ); - $this->expectOutputString( $expected_output ); - wp_print_font_faces_from_style_variations(); + $output = get_echo( 'wp_print_font_faces_from_style_variations' ); + $this->assertEqualHTML( $expected_output, $output ); } private function get_expected_styles_output( $styles ) { From 6c6a72b07dea9150d26ef9b7a767b22c67e83c5a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 23 Dec 2025 14:19:49 +0100 Subject: [PATCH 21/39] Use HTML API for hide header text --- src/wp-includes/theme.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index b8d35a255d35c..18b4d98b5ed73 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -3010,16 +3010,24 @@ function _custom_logo_header_styles() { $classes = array_map( 'sanitize_html_class', $classes ); $classes = '.' . implode( ', .', $classes ); - $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; - ?> - - - \n" + ); + $processor->next_tag(); + if ( ! current_theme_supports( 'html5', 'style' ) ) { + $processor->set_attribute( 'type', 'text/css' ); + } + $processor->set_modifiable_text( $css ); + echo $processor->get_updated_html(); } } From aad4744b50a4af3f714cb94ec2ea0cde3e455a81 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 23 Dec 2025 14:19:52 +0100 Subject: [PATCH 22/39] Revert "Use HTML API for hide header text" This reverts commit 6c6a72b07dea9150d26ef9b7a767b22c67e83c5a. --- src/wp-includes/theme.php | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 18b4d98b5ed73..b8d35a255d35c 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -3010,24 +3010,16 @@ function _custom_logo_header_styles() { $classes = array_map( 'sanitize_html_class', $classes ); $classes = '.' . implode( ', .', $classes ); - $css = <<<"CSS" - -{$classes} { - position: absolute; - clip-path: inset(50%); -} - -CSS; - - $processor = new WP_HTML_Tag_Processor( - "\n" - ); - $processor->next_tag(); - if ( ! current_theme_supports( 'html5', 'style' ) ) { - $processor->set_attribute( 'type', 'text/css' ); - } - $processor->set_modifiable_text( $css ); - echo $processor->get_updated_html(); + $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; + ?> + + + Date: Fri, 26 Dec 2025 16:46:59 +0100 Subject: [PATCH 23/39] Disable KSES content filter, prevent mangling CSS-in-JSON --- ...class-wp-rest-global-styles-controller.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 6a3405166a3d5..c566bebd74af1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -30,6 +30,44 @@ public function __construct( $post_type = 'wp_global_styles' ) { parent::__construct( $post_type ); } + /** + * Override method to prevent KSES HTML content filter from mangling CSS content in JSON. + * + * @inheritDoc + * + * @since 7.0.0 + */ + public function update_item( $request ) { + $priority = has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( false !== $priority ) { + remove_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); + } + $result = parent::update_item( $request ); + if ( false !== $priority ) { + add_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); + } + return $result; + } + + /** + * Override method to prevent KSES HTML content filter from mangling CSS content in JSON. + * + * @inheritDoc + * + * @since 7.0.0 + */ + public function create_item( $request ) { + $priority = has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( false !== $priority ) { + remove_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); + } + $result = parent::create_item( $request ); + if ( false !== $priority ) { + add_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); + } + return $result; + } + /** * Registers the controllers routes. * From 1ab58ec919c1b1568764b5a2ac959edfd86342af Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 26 Dec 2025 16:47:05 +0100 Subject: [PATCH 24/39] Revert "Disable KSES content filter, prevent mangling CSS-in-JSON" This reverts commit 96849ff908205522372bf6577a89ef927732b65c. --- ...class-wp-rest-global-styles-controller.php | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index c566bebd74af1..6a3405166a3d5 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -30,44 +30,6 @@ public function __construct( $post_type = 'wp_global_styles' ) { parent::__construct( $post_type ); } - /** - * Override method to prevent KSES HTML content filter from mangling CSS content in JSON. - * - * @inheritDoc - * - * @since 7.0.0 - */ - public function update_item( $request ) { - $priority = has_filter( 'content_save_pre', 'wp_filter_post_kses' ); - if ( false !== $priority ) { - remove_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); - } - $result = parent::update_item( $request ); - if ( false !== $priority ) { - add_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); - } - return $result; - } - - /** - * Override method to prevent KSES HTML content filter from mangling CSS content in JSON. - * - * @inheritDoc - * - * @since 7.0.0 - */ - public function create_item( $request ) { - $priority = has_filter( 'content_save_pre', 'wp_filter_post_kses' ); - if ( false !== $priority ) { - remove_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); - } - $result = parent::create_item( $request ); - if ( false !== $priority ) { - add_filter( 'content_save_pre', 'wp_filter_post_kses', $priority ); - } - return $result; - } - /** * Registers the controllers routes. * From ac2c8e6e0a672c2ba1e802810570bdc6305666fa Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 26 Dec 2025 16:55:28 +0100 Subject: [PATCH 25/39] Escape "<" and ">" in JSON to protect tag-like syntax --- src/wp-includes/kses.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 35327e1a01cce..24cc54ec819e1 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2474,7 +2474,12 @@ function wp_filter_global_styles_post( $data ) { $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' ); $data_to_encode['isGlobalStylesUserThemeJSON'] = true; - return wp_slash( wp_json_encode( $data_to_encode ) ); + /* + * JSON_UNESCAPED_SLASHES - There's no reason to escape the "/" character in this JSON. + * JSON_HEX_TAG - Unicode escape "<" and ">" to prevent HTML-oriented filters from mangling + * HTML tag-like CSS, e.g. `@property --my-prop { syntax: ""; }`. + */ + return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG ) ); } return $data; } From 4e88745117c1459f593cc29150392ffa021ef726 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 26 Dec 2025 22:45:33 +0100 Subject: [PATCH 26/39] Fix lint --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index e3d1c446e33b1..e63012b31f594 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2415,7 +2415,7 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - $processor = new WP_HTML_Tag_Processor( "" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $style_tag_contents = "\n{$wp_styles->print_code}\n" . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); From c8f8a4dbb6d35cdf11b921d93fe4faed66dc4b01 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 26 Dec 2025 17:02:13 +0100 Subject: [PATCH 27/39] Add test with bare & to trick KSES more --- .../tests/rest-api/rest-global-styles-controller.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index ab4395db02c2f..039e61309ea12 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -846,6 +846,7 @@ public function test_update_allows_valid_css_with_more_syntax() { inherits: true; initial-value: false; } +h1::before { content: "fun & games"; } CSS; $request->set_body_params( array( @@ -856,5 +857,10 @@ public function test_update_allows_valid_css_with_more_syntax() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( $css, $data['styles']['css'] ); + + // Compare expected API output to WP internal values. + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( $css, $response->get_data()['styles']['css'] ); } } From 731bce7f2db1a1f9d97178f23483a0fe039759f3 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 26 Dec 2025 17:09:04 +0100 Subject: [PATCH 28/39] Escape "&" in JSON to protect & and things that appear to be HTML character references --- src/wp-includes/kses.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 24cc54ec819e1..b2c381e4ba500 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2478,8 +2478,10 @@ function wp_filter_global_styles_post( $data ) { * JSON_UNESCAPED_SLASHES - There's no reason to escape the "/" character in this JSON. * JSON_HEX_TAG - Unicode escape "<" and ">" to prevent HTML-oriented filters from mangling * HTML tag-like CSS, e.g. `@property --my-prop { syntax: ""; }`. + * JSON_HEX_TAG - Unicode escape "&" to prevent HTML-oriented filters from mangling CSS with + * HTML special characters, e.g. `*::before { content: "fun & games"; }`. */ - return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG ) ); + return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) ); } return $data; } From 99b1ba49dac459747038d81d77f7e589ed2f89f5 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 26 Dec 2025 22:45:33 +0100 Subject: [PATCH 29/39] Fix lint --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index e3d1c446e33b1..e63012b31f594 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2415,7 +2415,7 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - $processor = new WP_HTML_Tag_Processor( "" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $style_tag_contents = "\n{$wp_styles->print_code}\n" . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); From 0050789bdffe89852ebbe67d792c560171612623 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 12:52:09 +0100 Subject: [PATCH 30/39] Revert HTML API STYLE tag generation This moves to https://github.com/WordPress/wordpress-develop/pull/10656 --- src/wp-includes/class-wp-styles.php | 20 ++++---- src/wp-includes/script-loader.php | 72 +++-------------------------- src/wp-includes/theme.php | 35 +++++++------- 3 files changed, 33 insertions(+), 94 deletions(-) diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index e19a7645f93e8..a5d071da54886 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -158,11 +158,11 @@ public function do_item( $handle, $group = false ) { $inline_style = $this->print_inline_style( $handle, false ); if ( $inline_style ) { - $processor = new WP_HTML_Tag_Processor( "\n" ); - $processor->next_tag(); - $processor->set_attribute( 'id', "{$handle}-inline-css" ); - $processor->set_modifiable_text( "\n{$inline_style}\n" ); - $inline_style_tag = $processor->get_updated_html(); + $inline_style_tag = sprintf( + "\n", + esc_attr( $handle ), + $inline_style + ); } else { $inline_style_tag = ''; } @@ -336,11 +336,11 @@ public function print_inline_style( $handle, $display = true ) { return $output; } - $processor = new WP_HTML_Tag_Processor( "\n" ); - $processor->next_tag(); - $processor->set_attribute( 'id', "{$handle}-inline-css" ); - $processor->set_modifiable_text( "\n{$output}\n" ); - echo $processor->get_updated_html(); + printf( + "\n", + esc_attr( $handle ), + $output + ); return true; } diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index e63012b31f594..a60fce6262744 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2210,10 +2210,8 @@ function _print_scripts() { if ( $concat ) { if ( ! empty( $wp_scripts->print_code ) ) { echo "\n\n"; } @@ -2415,12 +2413,10 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - $processor = new WP_HTML_Tag_Processor( '' ); - $processor->next_tag(); - $style_tag_contents = "\n{$wp_styles->print_code}\n" - . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); - $processor->set_modifiable_text( $style_tag_contents ); - echo $processor->get_updated_html(); + echo "\n"; } } @@ -2873,8 +2869,7 @@ function wp_enqueue_editor_format_library_assets() { * @return string String made of sanitized ` - * - * In an HTML document this would print "…" to the console, - * but in an XHTML document it would print "…" to the console. - * - * - * - * In an HTML document this would print "An image is in HTML", - * but it's an invalid XHTML document because it interprets the `` - * as an empty tag missing its closing `/`. - * - * @see https://www.w3.org/TR/xhtml1/#h-4.8 - */ - if ( - ! current_theme_supports( 'html5', 'script' ) && - ( - ! isset( $attributes['type'] ) || - 'module' === $attributes['type'] || - str_contains( $attributes['type'], 'javascript' ) || - str_contains( $attributes['type'], 'ecmascript' ) || - str_contains( $attributes['type'], 'jscript' ) || - str_contains( $attributes['type'], 'livescript' ) - ) - ) { - /* - * If the string `]]>` exists within the JavaScript it would break - * out of any wrapping CDATA section added here, so to start, it's - * necessary to escape that sequence which requires splitting the - * content into two CDATA sections wherever it's found. - * - * Note: it's only necessary to escape the closing `]]>` because - * an additional `', ']]]]>', $data ); - - // Wrap the entire escaped script inside a CDATA section. - $data = sprintf( "/* */", $data ); - } - $data = "\n" . trim( $data, "\n\r " ) . "\n"; /** @@ -3201,10 +3146,7 @@ function wp_enqueue_block_support_styles( $style, $priority = 10 ) { add_action( $action_hook_name, static function () use ( $style ) { - $processor = new WP_HTML_Tag_Processor( "\n" ); - $processor->next_tag(); - $processor->set_modifiable_text( $style ); - echo $processor->get_updated_html(); + echo "\n"; }, $priority ); diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index adf3b50555118..89d56d4e44bce 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1950,13 +1950,11 @@ function _custom_background_cb() { $style .= $image . $position . $size . $repeat . $attachment; } - - $processor = new WP_HTML_Tag_Processor( "" ); - $processor->next_tag(); - - $style_tag_content = 'body.custom-background { ' . trim( $style ) . ' }'; - $processor->set_modifiable_text( "\n{$style_tag_content}\n" ); - echo $processor->get_updated_html(); + ?> + id="custom-background-css"> +body.custom-background { } + + ' ); - $processor->next_tag(); - if ( ! current_theme_supports( 'html5', 'style' ) ) { - $processor->set_attribute( 'type', 'text/css' ); - } - $processor->set_attribute( 'id', 'wp-custom-css' ); - $processor->set_modifiable_text( "\n{$styles}\n" ); - echo $processor->get_updated_html(); + if ( $styles || is_customize_preview() ) : + $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; + ?> + id="wp-custom-css"> + + + Date: Mon, 29 Dec 2025 14:51:15 +0100 Subject: [PATCH 31/39] Improve invalid CSS message. --- .../customize/class-wp-customize-custom-css-setting.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index ebe61fbf270f6..ccbf9d876f701 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -172,7 +172,7 @@ public function validate( $value ) { * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm */ if ( false !== stripos( $css, 'add( 'illegal_markup', __( 'CSS must not contain possible closing STYLE tag "add( 'illegal_markup', __( 'CSS must not contain the text </style.' ) ); } if ( ! $validity->has_errors() ) { From 96ea3c44c528f14845c342e7b8de4b6dd329f1ed Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 15:39:51 +0100 Subject: [PATCH 32/39] Remove arbitrary CSS content validation The HTML API will make the content safe to print. The validation serves no purpose. --- ...class-wp-rest-global-styles-controller.php | 17 +------------- .../rest-global-styles-controller.php | 22 ------------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 6a3405166a3d5..f8b8af4822bda 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -663,27 +663,12 @@ public function get_theme_items( $request ) { * * @since 6.2.0 * @since 6.4.0 Changed method visibility to protected. - * @since 7.0.0 Relaxed to only check for safe HTML STYLE tag contents. + * @since 7.0.0 Allow arbitrary CSS content. * * @param string $css CSS to validate. * @return true|WP_Error True if the input was validated, otherwise WP_Error. */ protected function validate_custom_css( $css ) { - /** - * Check for a closing STYLE tag inside the CSS. - * - * STYLE tags are processed using the "generic raw text parsing algorithm." They contain - * raw text up until a matching closing tag. - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm - */ - if ( false !== stripos( $css, ' 400 ) - ); - } return true; } } diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index 039e61309ea12..126ecd28bf53b 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -647,28 +647,6 @@ public function test_update_item_valid_styles_css() { $this->assertSame( 'body { color: red; }', $data['styles']['css'] ); } - /** - * @covers WP_REST_Global_Styles_Controller::update_item - * @ticket 57536 - * @ticket 64418 - */ - public function test_update_item_invalid_styles_css() { - wp_set_current_user( self::$admin_id ); - if ( is_multisite() ) { - grant_super_admin( self::$admin_id ); - } - $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); - $request->set_body_params( - array( - 'styles' => array( - 'css' => 'dispatch( $request ); - $this->assertErrorResponse( 'rest_custom_css_illegal_markup', $response, 400 ); - } - /** * Tests the submission of a custom block style variation that was defined * within a theme style variation and wouldn't be registered at the time From 56e19b5f3a1910e06dbdb3e3d73c5211ee411a79 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 15:40:30 +0100 Subject: [PATCH 33/39] Revert customizer custom_css changes --- .../class-wp-customize-custom-css-setting.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php index ccbf9d876f701..aab0e475304ea 100644 --- a/src/wp-includes/customize/class-wp-customize-custom-css-setting.php +++ b/src/wp-includes/customize/class-wp-customize-custom-css-setting.php @@ -145,14 +145,14 @@ public function value() { } /** - * Validate a received value for being safe HTML STYLE tag contents. + * Validate a received value for being valid CSS. * + * Checks for imbalanced braces, brackets, and comments. * Notifications are rendered when the customizer state is saved. * * @since 4.7.0 * @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor. * @since 5.9.0 Renamed `$css` to `$value` for PHP 8 named parameter support. - * @since 7.0.0 Relaxed to only check for safe HTML STYLE tag contents. * * @param string $value CSS to validate. * @return true|WP_Error True if the input was validated, otherwise WP_Error. @@ -163,16 +163,8 @@ public function validate( $value ) { $validity = new WP_Error(); - /** - * Check for a closing STYLE tag inside the CSS. - * - * STYLE tags are processed using the "generic raw text parsing algorithm." They contain - * raw text up until a matching closing tag. - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm - */ - if ( false !== stripos( $css, 'add( 'illegal_markup', __( 'CSS must not contain the text </style.' ) ); + if ( preg_match( '#add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) ); } if ( ! $validity->has_errors() ) { From d9ae831c0858a7b3c0c9b106d62e306b7ab396eb Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 16:01:04 +0100 Subject: [PATCH 34/39] Encode data in the prepare_for_database method --- src/wp-includes/kses.php | 7 ++----- .../endpoints/class-wp-rest-global-styles-controller.php | 9 ++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index b2c381e4ba500..ca631450c2622 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2475,11 +2475,8 @@ function wp_filter_global_styles_post( $data ) { $data_to_encode['isGlobalStylesUserThemeJSON'] = true; /* - * JSON_UNESCAPED_SLASHES - There's no reason to escape the "/" character in this JSON. - * JSON_HEX_TAG - Unicode escape "<" and ">" to prevent HTML-oriented filters from mangling - * HTML tag-like CSS, e.g. `@property --my-prop { syntax: ""; }`. - * JSON_HEX_TAG - Unicode escape "&" to prevent HTML-oriented filters from mangling CSS with - * HTML special characters, e.g. `*::before { content: "fun & games"; }`. + * JSON encode the data stored in post content. + * Encode characters that are likely be mangled by HTML filters: "<>&". */ return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index f8b8af4822bda..510d66aeed5b2 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -275,7 +275,14 @@ protected function prepare_item_for_database( $request ) { } $config['isGlobalStylesUserThemeJSON'] = true; $config['version'] = WP_Theme_JSON::LATEST_SCHEMA; - $changes->post_content = wp_json_encode( $config ); + /** + * JSON encode the data stored in post content. + * Escape characters that are likely be mangled by HTML filters: "<>&". + * + * This data is later re-encoded by {@see wp_filter_global_styles_post}. + * The escaping is also applied here as a precaution. + */ + $changes->post_content = wp_json_encode( $config, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); } // Post title. From 0141653ab81876a8d7930db11aadaff15cf5bdf6 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 18:08:54 +0100 Subject: [PATCH 35/39] Restore STYLE tag trailing newline Co-authored-by: Weston Ruter --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index bd4f15359680c..c38d662666ee0 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2413,7 +2413,7 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - $processor = new WP_HTML_Tag_Processor( '' ); + $processor = new WP_HTML_Tag_Processor( "\n" ); $processor->next_tag(); $style_tag_contents = "\n{$wp_styles->print_code}\n" . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); From 658509902d218a1bf3a721a162ddd6c5a34bbd4a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 19:47:30 +0100 Subject: [PATCH 36/39] Restore STYLE tag trailing newlines in theme.php Co-authored-by: Weston Ruter --- src/wp-includes/theme.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index adf3b50555118..44fd522aaeaae 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1951,7 +1951,7 @@ function _custom_background_cb() { $style .= $image . $position . $size . $repeat . $attachment; } - $processor = new WP_HTML_Tag_Processor( "" ); + $processor = new WP_HTML_Tag_Processor( "\n" ); $processor->next_tag(); $style_tag_content = 'body.custom-background { ' . trim( $style ) . ' }'; @@ -1970,7 +1970,7 @@ function wp_custom_css_cb() { return; } - $processor = new WP_HTML_Tag_Processor( '' ); + $processor = new WP_HTML_Tag_Processor( "\n" ); $processor->next_tag(); if ( ! current_theme_supports( 'html5', 'style' ) ) { $processor->set_attribute( 'type', 'text/css' ); From 30ca7cb6511da258551926d0bc37b8b5593eedef Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Dec 2025 19:52:37 +0100 Subject: [PATCH 37/39] Cross-reference escaping in REST controller --- src/wp-includes/kses.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index ca631450c2622..31aae08740ed4 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2474,9 +2474,11 @@ function wp_filter_global_styles_post( $data ) { $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' ); $data_to_encode['isGlobalStylesUserThemeJSON'] = true; - /* + /** * JSON encode the data stored in post content. - * Encode characters that are likely be mangled by HTML filters: "<>&". + * Escape characters that are likely be mangled by HTML filters: "<>&". + * + * This matches the escaping in {@see WP_REST_Global_Styles_Controller::prepare_item_for_database}. */ return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) ); } From e1322b09a8ef63db105ad0c89c5acabbd3c1f3a6 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 30 Dec 2025 10:34:50 +0100 Subject: [PATCH 38/39] Deprecate validate_custom_css method The method serves no purpose and can be deprecated. --- .../class-wp-rest-global-styles-controller.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 510d66aeed5b2..b66ae3537ba1d 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -253,12 +253,6 @@ protected function prepare_item_for_database( $request ) { if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { $config = array(); if ( isset( $request['styles'] ) ) { - if ( isset( $request['styles']['css'] ) ) { - $css_validation_result = $this->validate_custom_css( $request['styles']['css'] ); - if ( is_wp_error( $css_validation_result ) ) { - return $css_validation_result; - } - } $config['styles'] = $request['styles']; } elseif ( isset( $existing_config['styles'] ) ) { $config['styles'] = $existing_config['styles']; @@ -664,18 +658,17 @@ public function get_theme_items( $request ) { } /** - * Validate style.css as valid CSS. - * - * Currently just checks that CSS will not break an HTML STYLE tag. - * * @since 6.2.0 * @since 6.4.0 Changed method visibility to protected. - * @since 7.0.0 Allow arbitrary CSS content. + * @deprecated 7.0.0 This method is deprecated and always returns true. + * + * @ignore * * @param string $css CSS to validate. - * @return true|WP_Error True if the input was validated, otherwise WP_Error. + * @return true|WP_Error Always returns true. */ protected function validate_custom_css( $css ) { + _deprecated_function( __METHOD__, '7.0.0' ); return true; } } From 407d43f838826d709305b4da8e92d4d7d6210154 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 30 Dec 2025 10:42:53 +0100 Subject: [PATCH 39/39] Move trailing newline out of Tag Processor --- src/wp-includes/class-wp-styles.php | 8 ++++---- src/wp-includes/fonts/class-wp-font-face.php | 4 ++-- src/wp-includes/script-loader.php | 8 ++++---- src/wp-includes/theme.php | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index e19a7645f93e8..72af6c29b0171 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -158,11 +158,11 @@ public function do_item( $handle, $group = false ) { $inline_style = $this->print_inline_style( $handle, false ); if ( $inline_style ) { - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $processor->set_attribute( 'id', "{$handle}-inline-css" ); $processor->set_modifiable_text( "\n{$inline_style}\n" ); - $inline_style_tag = $processor->get_updated_html(); + $inline_style_tag = "{$processor->get_updated_html()}\n"; } else { $inline_style_tag = ''; } @@ -336,11 +336,11 @@ public function print_inline_style( $handle, $display = true ) { return $output; } - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $processor->set_attribute( 'id', "{$handle}-inline-css" ); $processor->set_modifiable_text( "\n{$output}\n" ); - echo $processor->get_updated_html(); + echo "{$processor->get_updated_html()}\n"; return true; } diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 4c3100e66992b..193a5d0951ddb 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -92,10 +92,10 @@ public function generate_and_print( array $fonts ) { return; } - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $processor->set_modifiable_text( "\n{$css}\n" ); - echo $processor->get_updated_html(); + echo "{$processor->get_updated_html()}\n"; } /** diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index c38d662666ee0..fb9cebbf4b551 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2413,12 +2413,12 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $style_tag_contents = "\n{$wp_styles->print_code}\n" . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); $processor->set_modifiable_text( $style_tag_contents ); - echo $processor->get_updated_html(); + echo "{$processor->get_updated_html()}\n"; } } @@ -3148,10 +3148,10 @@ function wp_enqueue_block_support_styles( $style, $priority = 10 ) { add_action( $action_hook_name, static function () use ( $style ) { - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $processor->set_modifiable_text( $style ); - echo $processor->get_updated_html(); + echo "{$processor->get_updated_html()}\n"; }, $priority ); diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 44fd522aaeaae..0ff915cbe4263 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1951,12 +1951,12 @@ function _custom_background_cb() { $style .= $image . $position . $size . $repeat . $attachment; } - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( "" ); $processor->next_tag(); $style_tag_content = 'body.custom-background { ' . trim( $style ) . ' }'; $processor->set_modifiable_text( "\n{$style_tag_content}\n" ); - echo $processor->get_updated_html(); + echo "{$processor->get_updated_html()}\n"; } /** @@ -1970,14 +1970,14 @@ function wp_custom_css_cb() { return; } - $processor = new WP_HTML_Tag_Processor( "\n" ); + $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); if ( ! current_theme_supports( 'html5', 'style' ) ) { $processor->set_attribute( 'type', 'text/css' ); } $processor->set_attribute( 'id', 'wp-custom-css' ); $processor->set_modifiable_text( "\n{$styles}\n" ); - echo $processor->get_updated_html(); + echo "{$processor->get_updated_html()}\n"; } /**