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
This commit is contained in:
jonsurrell
2026-01-15 12:09:51 +00:00
parent 0f1223ce1f
commit e8a6e1f192
5 changed files with 126 additions and 48 deletions

View File

@@ -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',

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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.