Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -2077,6 +2077,17 @@ function _filter_block_content_callback( $matches ) {
function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) {
$block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols, $block );
Copy link
Member

@ramonjd ramonjd Mar 2, 2026

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 😄 )


// Per-block custom CSS (attrs.style.css) may contain & and > as valid
// CSS selectors. wp_kses() entity-encodes these because it treats the
// value as HTML. Decode them after KSES has already stripped any
// dangerous HTML tags, so the CSS round-trips correctly through
// serialize_block_attributes().
if ( isset( $block['attrs']['style']['css'] ) ) {
$block['attrs']['style']['css'] = undo_block_custom_css_kses_entities(
$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 Expand Up @@ -2124,6 +2135,40 @@ function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = ar
return $value;
}

/**
* Decodes HTML entities in per-block custom CSS that were incorrectly
* introduced by wp_kses() during the block KSES filtering pipeline.
*
* Per-block custom CSS (stored in attrs.style.css) may contain & and >
* as valid CSS selectors (nesting and child combinator). When wp_kses()
* processes this CSS string as if it were HTML, it entity-encodes these
* characters (&, >). If the block is then re-serialized via
* serialize_block_attributes(), the entity's ampersand is escaped again
* (\u0026amp;), producing a double-encoded value that corrupts the CSS
* on subsequent editor loads.
*
* This reverses only the specific named entities that wp_kses() may
* introduce, intentionally narrower than wp_specialchars_decode() to
* avoid decoding numeric/hex references that KSES intentionally preserved.
*
* @since 7.0
*
* @param string $value Per-block custom CSS string potentially containing
* KSES-introduced entities.
* @return string CSS string with KSES-introduced entities decoded.
*/
function undo_block_custom_css_kses_entities( $value ) {
if ( ! is_string( $value ) || false === strpos( $value, '&' ) ) {
return $value;
}

return str_replace(
array( '&', '>', '"', ''' ),
array( '&', '>', '"', "'" ),
Copy link
Author

@glendaviesnz glendaviesnz Mar 1, 2026

Choose a reason for hiding this comment

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

We will not doubt need to account for other values here, but we can work out exactly what needs to be covered once there is some agreement on the best way to solve this problem - I imagine there will be a smarter solution - so didn't spend too much time finessing this one.

$value
);
}

/**
* Sanitizes the value of the Template Part block's `tagName` attribute.
*
Expand Down
Loading