@@ -2077,6 +2077,17 @@ function _filter_block_content_callback( $matches ) {
20772077function filter_block_kses ( $ block , $ allowed_html , $ allowed_protocols = array () ) {
20782078 $ block ['attrs ' ] = filter_block_kses_value ( $ block ['attrs ' ], $ allowed_html , $ allowed_protocols , $ block );
20792079
2080+ // Per-block custom CSS (attrs.style.css) may contain & and > as valid
2081+ // CSS selectors. wp_kses() entity-encodes these because it treats the
2082+ // value as HTML. Decode them after KSES has already stripped any
2083+ // dangerous HTML tags, so the CSS round-trips correctly through
2084+ // serialize_block_attributes().
2085+ if ( isset ( $ block ['attrs ' ]['style ' ]['css ' ] ) ) {
2086+ $ block ['attrs ' ]['style ' ]['css ' ] = undo_block_custom_css_kses_entities (
2087+ $ block ['attrs ' ]['style ' ]['css ' ]
2088+ );
2089+ }
2090+
20802091 if ( is_array ( $ block ['innerBlocks ' ] ) ) {
20812092 foreach ( $ block ['innerBlocks ' ] as $ i => $ inner_block ) {
20822093 $ block ['innerBlocks ' ][ $ i ] = filter_block_kses ( $ inner_block , $ allowed_html , $ allowed_protocols );
@@ -2124,6 +2135,40 @@ function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = ar
21242135 return $ value ;
21252136}
21262137
2138+ /**
2139+ * Decodes HTML entities in per-block custom CSS that were incorrectly
2140+ * introduced by wp_kses() during the block KSES filtering pipeline.
2141+ *
2142+ * Per-block custom CSS (stored in attrs.style.css) may contain & and >
2143+ * as valid CSS selectors (nesting and child combinator). When wp_kses()
2144+ * processes this CSS string as if it were HTML, it entity-encodes these
2145+ * characters (&, >). If the block is then re-serialized via
2146+ * serialize_block_attributes(), the entity's ampersand is escaped again
2147+ * (\u0026amp;), producing a double-encoded value that corrupts the CSS
2148+ * on subsequent editor loads.
2149+ *
2150+ * This reverses only the specific named entities that wp_kses() may
2151+ * introduce, intentionally narrower than wp_specialchars_decode() to
2152+ * avoid decoding numeric/hex references that KSES intentionally preserved.
2153+ *
2154+ * @since 6.9.0
2155+ *
2156+ * @param string $value Per-block custom CSS string potentially containing
2157+ * KSES-introduced entities.
2158+ * @return string CSS string with KSES-introduced entities decoded.
2159+ */
2160+ function undo_block_custom_css_kses_entities ( $ value ) {
2161+ if ( ! is_string ( $ value ) || false === strpos ( $ value , '& ' ) ) {
2162+ return $ value ;
2163+ }
2164+
2165+ return str_replace (
2166+ array ( '& ' , '> ' , '" ' , '' ' ),
2167+ array ( '& ' , '> ' , '" ' , "' " ),
2168+ $ value
2169+ );
2170+ }
2171+
21272172/**
21282173 * Sanitizes the value of the Template Part block's `tagName` attribute.
21292174 *
0 commit comments