From f058e793d0994f9e1ce5336bc9a1189697f52c3b Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 3 Mar 2026 16:31:40 +1100 Subject: [PATCH 1/2] Skip KSES for block custom CSS; add wp_validate_css_for_style_element() Per-block attrs.style.css is sanitized with strip_tags and the shared STYLE-element validator instead of KSES so it isn't entity-encoded. Customizer and REST global styles now use the same validation helper. --- src/wp-includes/block-supports/custom-css.php | 4 +- src/wp-includes/blocks.php | 13 +++ .../class-wp-customize-custom-css-setting.php | 84 ++----------------- src/wp-includes/formatting.php | 63 ++++++++++++++ ...class-wp-rest-global-styles-controller.php | 79 ++--------------- 5 files changed, 91 insertions(+), 152 deletions(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 9d5b13426f4ef..05bd657a873c2 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -26,8 +26,8 @@ function wp_render_custom_css_support_styles( $parsed_block ) { return $parsed_block; } - // Validate CSS doesn't contain HTML markup (same validation as global styles REST API). - if ( preg_match( '# $inner_block ) { $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); @@ -2092,6 +2104,7 @@ function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() * * @since 5.3.1 * @since 6.5.5 Added the `$block_context` parameter. + * @since 7.0.0 Per-block custom CSS (attrs.style.css) is no longer passed here; see filter_block_kses(). * * @param string[]|string $value The attribute value to filter. * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 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 58897a7e72742..32efa61ffec7e 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 @@ -153,10 +153,9 @@ public function value() { * @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 Only restricts contents which risk prematurely closing the STYLE element, - * either through a STYLE end tag or a prefix of one which might become a - * full end tag when combined with the contents of other styles. + * @since 7.0.0 Delegates to wp_validate_css_for_style_element(). * + * @see wp_validate_css_for_style_element() * @see WP_REST_Global_Styles_Controller::validate_custom_css() * * @param string $value CSS to validate. @@ -166,81 +165,14 @@ public function validate( $value ) { // Restores the more descriptive, specific name for use within this method. $css = $value; - $validity = new WP_Error(); - - $length = strlen( $css ); - for ( - $at = strcspn( $css, '<' ); - $at < $length; - $at += strcspn( $css, '<', ++$at ) - ) { - $remaining_strlen = $length - $at; - /** - * Custom CSS text is expected to render inside an HTML STYLE element. - * A STYLE closing tag must not appear within the CSS text because it - * would close the element prematurely. - * - * The text must also *not* end with a partial closing tag (e.g., `<`, - * `` tag. - * - * Example: - * - * $style_a = 'p { font-weight: bold; add( - 'illegal_markup', - sprintf( - /* translators: %s is the CSS that was provided. */ - __( 'The CSS must not end in "%s".' ), - esc_html( substr( $css, $at ) ) - ) - ); - break; - } - - if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) { - $validity->add( - 'illegal_markup', - sprintf( - /* translators: %s is the CSS that was provided. */ - __( 'The CSS must not contain "%s".' ), - esc_html( substr( $css, $at, 8 ) ) - ) - ); - break; - } - } + $result = wp_validate_css_for_style_element( $css ); + if ( is_wp_error( $result ) ) { + $validity = new WP_Error(); + $validity->add( 'illegal_markup', $result->get_error_message() ); + return $validity; } - if ( ! $validity->has_errors() ) { - $validity = parent::validate( $css ); - } - return $validity; + return parent::validate( $css ); } /** diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 3b546c30eebd0..5c9e9f5c1ee1a 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5557,6 +5557,69 @@ function wp_strip_all_tags( $text, $remove_breaks = false ) { return trim( $text ); } +/** + * Validates that CSS is safe to output inside an HTML STYLE element. + * + * Rejects CSS that contains `` or a partial closing tag (e.g. ` 400 ) + ); + } + + if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) { + return new WP_Error( + 'rest_custom_css_illegal_markup', + sprintf( + /* translators: %s is the CSS that was provided. */ + __( 'The CSS must not contain "%s".' ), + esc_html( substr( $css, $at, 8 ) ) + ), + array( 'status' => 400 ) + ); + } + } + } + + return true; +} + /** * Sanitizes a string from user input or from the database. * 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 11b1478537ed7..82aa18957f5c8 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 @@ -666,89 +666,20 @@ public function get_theme_items( $request ) { /** * Validate style.css as valid CSS. * - * Currently just checks that CSS will not break an HTML STYLE tag. + * Delegates to wp_validate_css_for_style_element() so global styles, Customizer, + * and per-block custom CSS share the same validation rules. * * @since 6.2.0 * @since 6.4.0 Changed method visibility to protected. - * @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element, - * either through a STYLE end tag or a prefix of one which might become a - * full end tag when combined with the contents of other styles. + * @since 7.0.0 Delegates to wp_validate_css_for_style_element(). * + * @see wp_validate_css_for_style_element() * @see WP_Customize_Custom_CSS_Setting::validate() * * @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 ) { - $length = strlen( $css ); - for ( - $at = strcspn( $css, '<' ); - $at < $length; - $at += strcspn( $css, '<', ++$at ) - ) { - $remaining_strlen = $length - $at; - /** - * Custom CSS text is expected to render inside an HTML STYLE element. - * A STYLE closing tag must not appear within the CSS text because it - * would close the element prematurely. - * - * The text must also *not* end with a partial closing tag (e.g., `<`, - * `` tag. - * - * Example: - * - * $style_a = 'p { font-weight: bold; 400 ) - ); - } - - if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) { - return new WP_Error( - 'rest_custom_css_illegal_markup', - sprintf( - /* translators: %s is the CSS that was provided. */ - __( 'The CSS must not contain "%s".' ), - esc_html( substr( $css, $at, 8 ) ) - ), - array( 'status' => 400 ) - ); - } - } - } - - return true; + return wp_validate_css_for_style_element( $css ); } } From df1e26e86dd578a3cd40335a8f380135f9c3d002 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 3 Mar 2026 16:36:19 +1100 Subject: [PATCH 2/2] Comments restored in the helper --- src/wp-includes/formatting.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 5c9e9f5c1ee1a..43471208dbc08 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5583,6 +5583,34 @@ function wp_validate_css_for_style_element( $css ) { $at += 1 + strcspn( $css, '<', $at + 1 ) ) { $remaining_strlen = $length - $at; + /** + * Custom CSS text is expected to render inside an HTML STYLE element. + * A STYLE closing tag must not appear within the CSS text because it + * would close the element prematurely. + * + * The text must also *not* end with a partial closing tag (e.g., `<`, + * `` tag. + * + * Example: + * + * $style_a = 'p { font-weight: bold;