Commit c764fba909 for wordpress.org
commit c764fba9091ab1075c08b75b1789ee56f0da6a3c
Author: jonsurrell <jonsurrell@git.wordpress.org>
Date: Mon Jan 26 15:17:35 2026 +0000
Customize: Allow arbitrary custom CSS.
Update custom CSS validation to allow any CSS except `STYLE` close tags. Previously, some valid CSS would be rejected for containing HTML syntax characters, like this example:
{{{
@property --animate {
syntax: "<custom-ident>"; /* <-- Validation error on `<` */
inherits: true;
initial-value: false;
}
}}}
Developed in https://github.com/WordPress/wordpress-develop/pull/10667.
Follow-up to [61418], [61486].
Props jonsurrell, westonruter, peterwilsoncc, johnbillion, xknown, sabernhardt, dmsnell, soyebsalar01, dlh.
Fixes #64418.
Built from https://develop.svn.wordpress.org/trunk@61527
git-svn-id: http://core.svn.wordpress.org/trunk@60838 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/customize/class-wp-customize-custom-css-setting.php b/wp-includes/customize/class-wp-customize-custom-css-setting.php
index aab0e47530..58897a7e72 100644
--- a/wp-includes/customize/class-wp-customize-custom-css-setting.php
+++ b/wp-includes/customize/class-wp-customize-custom-css-setting.php
@@ -153,6 +153,11 @@ final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
* @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.
+ *
+ * @see WP_REST_Global_Styles_Controller::validate_custom_css()
*
* @param string $value CSS to validate.
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
@@ -163,8 +168,73 @@ final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
$validity = new WP_Error();
- if ( preg_match( '#</?\w+#', $css ) ) {
- $validity->add( 'illegal_markup', __( 'Markup is not allowed in 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., `<`,
+ * `</`, … `</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 styles 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 ) {
+ $validity->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;
+ }
+ }
}
if ( ! $validity->has_errors() ) {
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 e5f71ce3c7..a368bac776 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
@@ -674,6 +674,8 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
* 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.
*
+ * @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.
*/
@@ -707,7 +709,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
* 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
+ * through concatenation that these two styles combined to form content
* that would have broken out of the containing STYLE element, thus
* corrupting the page and potentially introducing security issues.
*
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 4ceaa77a76..30cf6b8393 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.0-alpha-61526';
+$wp_version = '7.0-alpha-61527';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.