Skip to content
5 changes: 0 additions & 5 deletions src/wp-includes/block-supports/custom-css.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ function wp_render_custom_css_support_styles( $parsed_block ) {
return $parsed_block;
}

// Validate CSS doesn't contain HTML markup (same validation as global styles REST API).
if ( preg_match( '#</?\w+#', $custom_css ) ) {
return $parsed_block;
}

// Generate a unique class name for this block instance.
$class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
$updated_class_name = isset( $parsed_block['attrs']['className'] )
Expand Down
23 changes: 23 additions & 0 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -2075,8 +2075,31 @@ function _filter_block_content_callback( $matches ) {
* @return array The filtered and sanitized block object result.
*/
function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) {
/*
* Per-block custom CSS (attrs.style.css) may contain characters like <, >,
* and & that are valid in CSS but would be mangled by wp_kses(), which treats
* all values as HTML. Encode these characters as JSON unicode escapes before
* KSES runs, then decode afterwards. This is the same approach used for
* Global Styles custom CSS (see r61486).
*/
$has_block_css = isset( $block['attrs']['style']['css'] );
if ( $has_block_css ) {
// wp_json_encode wraps the string in quotes: "encoded content".
// Trim them to get the raw escaped content as a PHP string.
$block['attrs']['style']['css'] = trim(
wp_json_encode( $block['attrs']['style']['css'], JSON_HEX_TAG | JSON_HEX_AMP ),
'"'
);
}

$block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols, $block );

@ramonjd ramonjd Mar 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting up a fix!

I was wondering: what is the important sanitization step for block CSS attributes?

wp_kses() treats the CSS string as HTML. But should it?

I'm wondering if an alternative solution would be to target the css attribute and run it through wp_strip_all_tags rather than wp_kses.

Or running through something similar (or reuse this same validation in a helper) that @sirreal and @dmsnell worked on in WP_REST_Global_Styles_Controller::validate_custom_css() for https://core.trac.wordpress.org/ticket/64418

There, for users without unfiltered_html, & and > in block custom CSS were being double-encoded by KSES + JSON, so the CSS broke.

(Sorry Jon and Dennis - you've become my default go-to brains trust for this stuff 😄 )


if ( $has_block_css && isset( $block['attrs']['style']['css'] ) ) {
$block['attrs']['style']['css'] = json_decode(
'"' . $block['attrs']['style']['css'] . '"'
);
}

if ( is_array( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $i => $inner_block ) {
$block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols );
Expand Down
Loading