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.