Make WordPress Core


Ignore:
Timestamp:
01/15/2026 12:08:01 PM (3 months ago)
Author:
jonsurrell
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php

    r61429 r61486  
    276276            $config['isGlobalStylesUserThemeJSON'] = true;
    277277            $config['version']                     = WP_Theme_JSON::LATEST_SCHEMA;
    278             $changes->post_content                 = wp_json_encode( $config );
     278            /**
     279             * JSON encode the data stored in post content.
     280             * Escape characters that are likely to be mangled by HTML filters: "<>&".
     281             *
     282             * This data is later re-encoded by {@see wp_filter_global_styles_post()}.
     283             * The escaping is also applied here as a precaution.
     284             */
     285            $changes->post_content = wp_json_encode( $config, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
    279286        }
    280287
     
    660667     * Validate style.css as valid CSS.
    661668     *
    662      * Currently just checks for invalid markup.
     669     * Currently just checks that CSS will not break an HTML STYLE tag.
    663670     *
    664671     * @since 6.2.0
    665672     * @since 6.4.0 Changed method visibility to protected.
     673     * @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
     674     *              either through a STYLE end tag or a prefix of one which might become a
     675     *              full end tag when combined with the contents of other styles.
    666676     *
    667677     * @param string $css CSS to validate.
     
    669679     */
    670680    protected function validate_custom_css( $css ) {
    671         if ( preg_match( '#</?\w+#', $css ) ) {
    672             return new WP_Error(
    673                 'rest_custom_css_illegal_markup',
    674                 __( 'Markup is not allowed in CSS.' ),
    675                 array( 'status' => 400 )
     681        $length = strlen( $css );
     682        for (
     683            $at = strcspn( $css, '<' );
     684            $at < $length;
     685            $at += strcspn( $css, '<', ++$at )
     686        ) {
     687            $remaining_strlen = $length - $at;
     688            /**
     689             * Custom CSS text is expected to render inside an HTML STYLE element.
     690             * A STYLE closing tag must not appear within the CSS text because it
     691             * would close the element prematurely.
     692             *
     693             * The text must also *not* end with a partial closing tag (e.g., `<`,
     694             * `</`, … `</style`) because subsequent styles which are concatenated
     695             * could complete it, forming a valid `</style>` tag.
     696             *
     697             * Example:
     698             *
     699             *     $style_a = 'p { font-weight: bold; </sty';
     700             *     $style_b = 'le> gotcha!';
     701             *     $combined = "{$style_a}{$style_b}";
     702             *
     703             *     $style_a = 'p { font-weight: bold; </style';
     704             *     $style_b = 'p > b { color: red; }';
     705             *     $combined = "{$style_a}\n{$style_b}";
     706             *
     707             * Note how in the second example, both of the style contents are benign
     708             * when analyzed on their own. The first style was likely the result of
     709             * improper truncation, while the second is perfectly sound. It was only
     710             * through concatenation that these two scripts combined to form content
     711             * that would have broken out of the containing STYLE element, thus
     712             * corrupting the page and potentially introducing security issues.
     713             *
     714             * @see https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
     715             */
     716            $possible_style_close_tag = 0 === substr_compare(
     717                $css,
     718                '</style',
     719                $at,
     720                min( 7, $remaining_strlen ),
     721                true
    676722            );
    677         }
     723            if ( $possible_style_close_tag ) {
     724                if ( $remaining_strlen < 8 ) {
     725                    return new WP_Error(
     726                        'rest_custom_css_illegal_markup',
     727                        sprintf(
     728                            /* translators: %s is the CSS that was provided. */
     729                            __( 'The CSS must not end in "%s".' ),
     730                            esc_html( substr( $css, $at ) )
     731                        ),
     732                        array( 'status' => 400 )
     733                    );
     734                }
     735
     736                if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
     737                    return new WP_Error(
     738                        'rest_custom_css_illegal_markup',
     739                        sprintf(
     740                            /* translators: %s is the CSS that was provided. */
     741                            __( 'The CSS must not contain "%s".' ),
     742                            esc_html( substr( $css, $at, 8 ) )
     743                        ),
     744                        array( 'status' => 400 )
     745                    );
     746                }
     747            }
     748        }
     749
    678750        return true;
    679751    }
Note: See TracChangeset for help on using the changeset viewer.