From f908771f33f0af24011c76ca73b8133987e9badc Mon Sep 17 00:00:00 2001 From: kristianwind Date: Sun, 20 Sep 2026 09:52:40 +0200 Subject: [PATCH] Take shared-menu.php 1.2.0: the glyph is CSS-escaped, not esc_attr'd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frozen by Nolimit SEO as 9012fe98, and it fixes something found here: the 1.1.0 glyph went through esc_attr() into a ', - esc_attr( WPMGR_MENU_SLUG ), - esc_attr( $glyph ) + preg_replace( '/[^a-z0-9_-]/', '', strtolower( WPMGR_MENU_SLUG ) ), + wpmgr_css_escape( $glyph ) ); + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * A string that is safe inside a CSS string literal, whatever it was. + * + * 🔴 esc_attr() is the wrong tool here and it protected by accident. + * Nolimit Discounts measured it: a glyph of `x"} body{display:none}` comes + * out as `x"} …`, the CSS string never closes, and nothing breaks + * out — but only because the entity happens to contain no quote. The CSS + * parser does not decode HTML entities, so it was never escaping in this + * context; it was a substitution that coincidentally removed the one + * character that mattered. Two things follow from that: a glyph containing + * a quote renders as the literal text `"` in the menu, and a single + * backslash — which esc_attr() leaves alone, and which IS an escape to the + * CSS parser — swallows the closing quote and the rest of the rule with + * it. Measured, not reasoned: the string then runs on to the quote in + * "Segoe UI" and the declaration block is corrupt. + * + * So do not filter the input; change its alphabet. Every character becomes + * a CSS numeric escape, so the emitted value contains nothing but + * backslashes, hex digits and spaces and cannot leave the string for any + * input at all. No whitelist to keep honest, no character class to get + * wrong, and an emoji still works. + * + * The decoder is written out rather than using mb_ord() because WordPress + * supports installs without mbstring, and ord() on a multi-byte character + * silently yields the first byte — which would render our own ∞ as + * mojibake on exactly those installs and on no machine any of us test on. + * + * @param string $text Text from a filter, i.e. arbitrary. + * @return string CSS escapes, safe between double quotes. + */ + function wpmgr_css_escape( $text ) { + $out = ''; + $len = strlen( $text ); + + for ( $i = 0; $i < $len; $i++ ) { + $byte = ord( $text[ $i ] ); + + if ( $byte < 0x80 ) { + $code = $byte; + $tail = 0; + } elseif ( $byte >= 0xF0 ) { + $code = $byte & 0x07; + $tail = 3; + } elseif ( $byte >= 0xE0 ) { + $code = $byte & 0x0F; + $tail = 2; + } elseif ( $byte >= 0xC0 ) { + $code = $byte & 0x1F; + $tail = 1; + } else { + continue; // A stray continuation byte is not a character. + } + + while ( $tail > 0 && $i + 1 < $len && 0x80 === ( ord( $text[ $i + 1 ] ) & 0xC0 ) ) { + ++$i; + $code = ( $code << 6 ) | ( ord( $text[ $i ] ) & 0x3F ); + --$tail; + } + + if ( $tail > 0 ) { + continue; // Truncated sequence. + } + + // The trailing space terminates the escape, so a following hex + // digit cannot be read as part of it. + $out .= sprintf( '\\%X ', $code ); + } + + return $out; } /**