From e8a6e1f192e560f92eeef984a60e6adb2782bd71 Mon Sep 17 00:00:00 2001 From: jonsurrell Date: Thu, 15 Jan 2026 12:09:51 +0000 Subject: [PATCH] Customize: Allow arbitrary CSS in global styles custom CSS. Relax Global Styles custom CSS filters to allow arbitrary CSS. Escape HTML characters `<>&` in Global Styles data to prevent it from being mangled by post content filters. The data is JSON encoded and stored in `post_content`. Filters operating on `post_content` expect it to contain HTML. Some KSES filters would otherwise remove essential CSS features like the `` CSS data type because they appear to be HTML tags. [61418] changed STYLE tag generation to use the HTML API for improved safety. Developed in https://github.com/WordPress/wordpress-develop/pull/10641. Props jonsurrell, dmsnell, westonruter, ramonopoly, oandregal, jorgefilipecosta, sabernhardt, soyebsalar01. See #64418. Built from https://develop.svn.wordpress.org/trunk@61486 git-svn-id: http://core.svn.wordpress.org/trunk@60798 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/css/dist/index.php | 36 ++++---- wp-includes/js/dist/script-modules/index.php | 42 ++++----- wp-includes/kses.php | 8 +- ...class-wp-rest-global-styles-controller.php | 86 +++++++++++++++++-- wp-includes/version.php | 2 +- 5 files changed, 126 insertions(+), 48 deletions(-) diff --git a/wp-includes/css/dist/index.php b/wp-includes/css/dist/index.php index e0020d536c..7e5f0fa55c 100644 --- a/wp-includes/css/dist/index.php +++ b/wp-includes/css/dist/index.php @@ -7,6 +7,11 @@ */ return array( + array( + 'handle' => 'wp-preferences', + 'path' => 'preferences/style', + 'dependencies' => array('wp-components'), + ), array( 'handle' => 'wp-nux', 'path' => 'nux/style', @@ -17,11 +22,6 @@ return array( 'path' => 'list-reusable-blocks/style', 'dependencies' => array('wp-components'), ), - array( - 'handle' => 'wp-preferences', - 'path' => 'preferences/style', - 'dependencies' => array('wp-components'), - ), array( 'handle' => 'wp-reusable-blocks', 'path' => 'reusable-blocks/style', @@ -57,36 +57,36 @@ return array( 'path' => 'block-directory/style', 'dependencies' => array('wp-block-editor', 'wp-components', 'wp-editor'), ), - array( - 'handle' => 'wp-media-utils', - 'path' => 'media-utils/style', - 'dependencies' => array('wp-components'), - ), array( 'handle' => 'wp-customize-widgets', 'path' => 'customize-widgets/style', 'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-preferences', 'wp-widgets'), ), - array( - 'handle' => 'wp-edit-widgets', - 'path' => 'edit-widgets/style', - 'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences', 'wp-widgets'), - ), array( 'handle' => 'wp-edit-post', 'path' => 'edit-post/style', 'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-commands', 'wp-components', 'wp-editor', 'wp-preferences', 'wp-widgets'), ), array( - 'handle' => 'wp-block-library', - 'path' => 'block-library/style', - 'dependencies' => array('wp-block-editor', 'wp-components', 'wp-patterns'), + 'handle' => 'wp-edit-widgets', + 'path' => 'edit-widgets/style', + 'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences', 'wp-widgets'), + ), + array( + 'handle' => 'wp-media-utils', + 'path' => 'media-utils/style', + 'dependencies' => array('wp-components'), ), array( 'handle' => 'wp-editor', 'path' => 'editor/style', 'dependencies' => array('wp-block-editor', 'wp-commands', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences'), ), + array( + 'handle' => 'wp-block-library', + 'path' => 'block-library/style', + 'dependencies' => array('wp-block-editor', 'wp-components', 'wp-patterns'), + ), array( 'handle' => 'wp-block-editor', 'path' => 'block-editor/style', diff --git a/wp-includes/js/dist/script-modules/index.php b/wp-includes/js/dist/script-modules/index.php index 98bb486813..9438e8294f 100644 --- a/wp-includes/js/dist/script-modules/index.php +++ b/wp-includes/js/dist/script-modules/index.php @@ -7,6 +7,21 @@ */ return array( + array( + 'id' => '@wordpress/interactivity', + 'path' => 'interactivity/index', + 'asset' => 'interactivity/index.min.asset.php', + ), + array( + 'id' => '@wordpress/latex-to-mathml', + 'path' => 'latex-to-mathml/index', + 'asset' => 'latex-to-mathml/index.min.asset.php', + ), + array( + 'id' => '@wordpress/latex-to-mathml/loader', + 'path' => 'latex-to-mathml/loader', + 'asset' => 'latex-to-mathml/loader.min.asset.php', + ), array( 'id' => '@wordpress/interactivity-router', 'path' => 'interactivity-router/index', @@ -18,9 +33,9 @@ return array( 'asset' => 'interactivity-router/full-page.min.asset.php', ), array( - 'id' => '@wordpress/core-abilities', - 'path' => 'core-abilities/index', - 'asset' => 'core-abilities/index.min.asset.php', + 'id' => '@wordpress/abilities', + 'path' => 'abilities/index', + 'asset' => 'abilities/index.min.asset.php', ), array( 'id' => '@wordpress/a11y', @@ -28,30 +43,15 @@ return array( 'asset' => 'a11y/index.min.asset.php', ), array( - 'id' => '@wordpress/interactivity', - 'path' => 'interactivity/index', - 'asset' => 'interactivity/index.min.asset.php', - ), - array( - 'id' => '@wordpress/abilities', - 'path' => 'abilities/index', - 'asset' => 'abilities/index.min.asset.php', + 'id' => '@wordpress/core-abilities', + 'path' => 'core-abilities/index', + 'asset' => 'core-abilities/index.min.asset.php', ), array( 'id' => '@wordpress/route', 'path' => 'route/index', 'asset' => 'route/index.min.asset.php', ), - array( - 'id' => '@wordpress/latex-to-mathml', - 'path' => 'latex-to-mathml/index', - 'asset' => 'latex-to-mathml/index.min.asset.php', - ), - array( - 'id' => '@wordpress/latex-to-mathml/loader', - 'path' => 'latex-to-mathml/loader', - 'asset' => 'latex-to-mathml/loader.min.asset.php', - ), array( 'id' => '@wordpress/edit-site-init', 'path' => 'edit-site-init/index', diff --git a/wp-includes/kses.php b/wp-includes/kses.php index 5c3e3d4021..fd489c06c7 100644 --- a/wp-includes/kses.php +++ b/wp-includes/kses.php @@ -2386,7 +2386,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/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 2a3d4d340d..e5f71ce3c7 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -275,7 +275,14 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller { } $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,87 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller { /** * 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 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. * * @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( '# 400 ) + $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; } } diff --git a/wp-includes/version.php b/wp-includes/version.php index 99d0f66e2c..a37945731e 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '7.0-alpha-61485'; +$wp_version = '7.0-alpha-61486'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.