Skip to content
Closed
Show file tree
Hide file tree
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
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 );
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 😄 )


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