diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 1d77491c299f1..b42dda2f42ef6 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2474,7 +2474,13 @@ 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 encode the data stored in post content. + * Escape characters that are likely to 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 ) ); } return $data; } 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 2a3d4d340db5a..33e3632e1c4de 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 to 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. @@ -659,22 +666,51 @@ 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 Allow more 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 ) { - if ( preg_match( '#?\w+#', $css ) ) { - return new WP_Error( - 'rest_custom_css_illegal_markup', - __( 'Markup is not allowed in CSS.' ), - array( 'status' => 400 ) - ); + $length = strlen( $css ); + for ( + $at = strcspn( $css, '<' ); + $at < $length; + $at += strcspn( $css, '<', ++$at ) + ) { + $remaining_strlen = $length - $at; + $possible_style_close_tag = 0 === substr_compare( $css, ' 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; } } 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..207b20f903372 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,7 @@ 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' => '' ), ) ); $response = rest_get_server()->dispatch( $request ); @@ -826,4 +827,113 @@ 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: "