Commit e8a6e1f192 for wordpress.org
commit e8a6e1f192e560f92eeef984a60e6adb2782bd71
Author: jonsurrell <jonsurrell@git.wordpress.org>
Date: Thu Jan 15 12:09:51 2026 +0000
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 `<custom-ident>` 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
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( '#</?\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;
+ /**
+ * 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., `<`,
+ * `</`, … `</style`) because subsequent styles which are concatenated
+ * could complete it, forming a valid `</style>` tag.
+ *
+ * Example:
+ *
+ * $style_a = 'p { font-weight: bold; </sty';
+ * $style_b = 'le> gotcha!';
+ * $combined = "{$style_a}{$style_b}";
+ *
+ * $style_a = 'p { font-weight: bold; </style';
+ * $style_b = 'p > b { color: red; }';
+ * $combined = "{$style_a}\n{$style_b}";
+ *
+ * Note how in the second example, both of the style contents are benign
+ * when analyzed on their own. The first style was likely the result of
+ * improper truncation, while the second is perfectly sound. It was only
+ * through concatenation that these two scripts combined to form content
+ * that would have broken out of the containing STYLE element, thus
+ * corrupting the page and potentially introducing security issues.
+ *
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
+ */
+ $possible_style_close_tag = 0 === substr_compare(
+ $css,
+ '</style',
+ $at,
+ min( 7, $remaining_strlen ),
+ true
);
+ if ( $possible_style_close_tag ) {
+ if ( $remaining_strlen < 8 ) {
+ return new WP_Error(
+ 'rest_custom_css_illegal_markup',
+ sprintf(
+ /* translators: %s is the CSS that was provided. */
+ __( 'The CSS must not end in "%s".' ),
+ esc_html( substr( $css, $at ) )
+ ),
+ array( 'status' => 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.