diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index f338df5dce..ae48d6455f 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -187,7 +187,7 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { foreach ( $minimums as $minimum ) { $style_rule = sprintf( '#%s { min-height: %dpx; }', - $element_id, + $this->escape_css( $element_id ), $minimum['height'] ); @@ -206,6 +206,79 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { } } + /** + * Escapes a CSS identifier. + * + * This is a PHP implementation of the CSS.escape() method in CSSOM, based on the + * JavaScript polyfill by Mathias Bynens. + * + * @since 1.0.0 + * @link https://drafts.csswg.org/cssom/#the-css.escape()-method + * @link https://github.com/mathiasbynens/CSS.escape + * @link https://mathiasbynens.be/notes/css-escapes + * @license MIT + * + * @param string $ident Identifier to escape. + * @return string Escaped identifier. + */ + private function escape_css( string $ident ): string { + $length = strlen( $ident ); + $result = ''; + $first_code_unit = $length > 0 ? ord( $ident[0] ) : 0; + + for ( $i = 0; $i < $length; $i++ ) { + $code_unit = ord( $ident[ $i ] ); + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD). + if ( 0x0000 === $code_unit ) { + $result .= "\u{FFFD}"; + continue; + } + + if ( + // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F... + $code_unit <= 0x001F || 0x007F === $code_unit || + // If the character is the first character and is in the range [0-9] (U+0030 to U+0039)... + ( 0 === $i && $code_unit >= 0x0030 && $code_unit <= 0x0039 ) || + // If the character is the second character and is in the range [0-9] (U+0030 to U+0039) and the first character is a `-` (U+002D)... + ( 1 === $i && $code_unit >= 0x0030 && $code_unit <= 0x0039 && 0x002D === $first_code_unit ) + ) { + $result .= '\\' . dechex( $code_unit ) . ' '; + continue; + } + + // If the character is the first character and is a `-` (U+002D), and there is no second character... + if ( + 0 === $i && + 1 === $length && + 0x002D === $code_unit + ) { + $result .= '\\' . $ident[ $i ]; + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A)... + if ( + $code_unit >= 0x0080 || + 0x002D === $code_unit || + 0x005F === $code_unit || + ( $code_unit >= 0x0030 && $code_unit <= 0x0039 ) || + ( $code_unit >= 0x0041 && $code_unit <= 0x005A ) || + ( $code_unit >= 0x0061 && $code_unit <= 0x007A ) + ) { + $result .= $ident[ $i ]; + continue; + } + + // Otherwise, the escaped character. + $result .= '\\' . $ident[ $i ]; + } + return $result; + } + /** * Gets preconnect URLs based on embed type. * diff --git a/plugins/embed-optimizer/load.php b/plugins/embed-optimizer/load.php index 5cb973ba1d..e0b5de07af 100644 --- a/plugins/embed-optimizer/load.php +++ b/plugins/embed-optimizer/load.php @@ -5,7 +5,7 @@ * Description: Optimizes the performance of embeds through lazy-loading, preconnecting, and reserving space to reduce layout shifts. * Requires at least: 6.6 * Requires PHP: 7.2 - * Version: 1.0.0-beta3 + * Version: 1.0.0-beta4 * Author: WordPress Performance Team * Author URI: https://make.wordpress.org/performance/ * License: GPLv2 or later @@ -71,7 +71,7 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } )( 'embed_optimizer_pending_plugin', - '1.0.0-beta3', + '1.0.0-beta4', static function ( string $version ): void { if ( defined( 'EMBED_OPTIMIZER_VERSION' ) ) { return; diff --git a/plugins/embed-optimizer/readme.txt b/plugins/embed-optimizer/readme.txt index 95d367fdc0..0ed3c4e74a 100644 --- a/plugins/embed-optimizer/readme.txt +++ b/plugins/embed-optimizer/readme.txt @@ -2,7 +2,7 @@ Contributors: wordpressdotorg Tested up to: 6.9 -Stable tag: 1.0.0-beta3 +Stable tag: 1.0.0-beta4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance, embeds, optimization-detective @@ -67,6 +67,12 @@ The [plugin source code](https://github.com/WordPress/performance/tree/trunk/plu == Changelog == += 1.0.0-beta4 = + +**Security** + +* Add escaping for ID selector in styles added to reduce layout shifts. This fixes an XSS security vulnerability which required an authenticated user with at least a contributor role. Props to [duc193](https://github.com/nduc193) for [responsible disclosure](https://github.com/WordPress/performance/blob/trunk/SECURITY.md). ([2397](https://github.com/WordPress/performance/pull/2397)) + = 1.0.0-beta3 = **Enhancements** diff --git a/plugins/embed-optimizer/tests/test-cases/figures-with-fancy-ids/buffer.html b/plugins/embed-optimizer/tests/test-cases/figures-with-fancy-ids/buffer.html new file mode 100644 index 0000000000..3275256bf8 --- /dev/null +++ b/plugins/embed-optimizer/tests/test-cases/figures-with-fancy-ids/buffer.html @@ -0,0 +1,44 @@ + +
+ +