Make WordPress Core


Ignore:
Timestamp:
01/26/2026 03:17:00 PM (4 months ago)
Author:
jonsurrell
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/customize/class-wp-customize-custom-css-setting.php

    r60215 r61527  
    154154     * @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor.
    155155     * @since 5.9.0 Renamed `$css` to `$value` for PHP 8 named parameter support.
     156     * @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
     157     *              either through a STYLE end tag or a prefix of one which might become a
     158     *              full end tag when combined with the contents of other styles.
     159     *
     160     * @see WP_REST_Global_Styles_Controller::validate_custom_css()
    156161     *
    157162     * @param string $value CSS to validate.
     
    164169        $validity = new WP_Error();
    165170
    166         if ( preg_match( '#</?\w+#', $css ) ) {
    167             $validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) );
     171        $length = strlen( $css );
     172        for (
     173            $at = strcspn( $css, '<' );
     174            $at < $length;
     175            $at += strcspn( $css, '<', ++$at )
     176        ) {
     177            $remaining_strlen = $length - $at;
     178            /**
     179             * Custom CSS text is expected to render inside an HTML STYLE element.
     180             * A STYLE closing tag must not appear within the CSS text because it
     181             * would close the element prematurely.
     182             *
     183             * The text must also *not* end with a partial closing tag (e.g., `<`,
     184             * `</`, … `</style`) because subsequent styles which are concatenated
     185             * could complete it, forming a valid `</style>` tag.
     186             *
     187             * Example:
     188             *
     189             *     $style_a = 'p { font-weight: bold; </sty';
     190             *     $style_b = 'le> gotcha!';
     191             *     $combined = "{$style_a}{$style_b}";
     192             *
     193             *     $style_a = 'p { font-weight: bold; </style';
     194             *     $style_b = 'p > b { color: red; }';
     195             *     $combined = "{$style_a}\n{$style_b}";
     196             *
     197             * Note how in the second example, both of the style contents are benign
     198             * when analyzed on their own. The first style was likely the result of
     199             * improper truncation, while the second is perfectly sound. It was only
     200             * through concatenation that these two styles combined to form content
     201             * that would have broken out of the containing STYLE element, thus
     202             * corrupting the page and potentially introducing security issues.
     203             *
     204             * @see https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
     205             */
     206            $possible_style_close_tag = 0 === substr_compare(
     207                $css,
     208                '</style',
     209                $at,
     210                min( 7, $remaining_strlen ),
     211                true
     212            );
     213            if ( $possible_style_close_tag ) {
     214                if ( $remaining_strlen < 8 ) {
     215                    $validity->add(
     216                        'illegal_markup',
     217                        sprintf(
     218                            /* translators: %s is the CSS that was provided. */
     219                            __( 'The CSS must not end in "%s".' ),
     220                            esc_html( substr( $css, $at ) )
     221                        )
     222                    );
     223                    break;
     224                }
     225
     226                if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
     227                    $validity->add(
     228                        'illegal_markup',
     229                        sprintf(
     230                            /* translators: %s is the CSS that was provided. */
     231                            __( 'The CSS must not contain "%s".' ),
     232                            esc_html( substr( $css, $at, 8 ) )
     233                        )
     234                    );
     235                    break;
     236                }
     237            }
    168238        }
    169239
Note: See TracChangeset for help on using the changeset viewer.