This blog post is about the WooCommerce and Jetpack plugin vulnerability. If you’re a WooCommerce and Jetpack user, please update the plugin to at least version 8.2.0 and 12.8-a.3 respectively.
The plugin WooCommerce (versions <= 8.1.1, free version), which has over 5 million active installations, is known as the most popular open-source eCommerce solution in WordPress. The other plugin Jetpack (versions <= 12.8-a.1, free version) also has over 5 million active installations and is known as the most installed security, performance, marketing, and design tools plugin in WordPress. Both plugins are developed by Automattic.
WooCommerce plugin is commonly used to create online e-commerce shops. With this plugin, anyone can turn their regular website into a fully functioning online store, complete with all the necessary e-commerce features. WooCommerce also allows users to manage their online stores easily. From setting up product displays, and managing orders, to accepting multiple payment gateways.
For the Jetpack plugin, it provides security, performance, and growth tools for WordPress sites. This plugin offers several features such as a site activity log, daily backups, spam prevention tools, etc.
The security vulnerability
Both of the plugins suffer from an authenticated cross-site scripting (XSS) vulnerability. It allows a user with a minimum user role of Contributor to inject arbitrary JavaScript code into the website and could result from stealing sensitive information to, in this case, privilege escalation on the WordPress site.
This vulnerability occurs because there is a lack of output escaping and sanitization on the registered Gutenberg blocks. Gutenberg blocks itself allow us to build our own custom posts and pages without any additional custom code. The described vulnerability was fixed in WooCommerce version 8.2.0 and Jetpack version 12.8-a.3. The vulnerability on the WooCommerce and Jetpack plugins have been assigned CVE-2023-47777 and CVE-2023-45050 respectively.
We found the vulnerability in multiple areas of the code that handle the custom Gutenberg block process. The vulnerable code specifically exists in the block attributes parameter that could be supplied by the user with a Contributor role when constructing a custom block. The vulnerability itself originally existed on the WooCommerce Blocks component which also exists as a standalone plugin.
The first vulnerable case is from wp:woocommerce/featured-product block and located on render_image and render_bg_image function:
The affected variable on render_image function is $attributes['alt'] and $style. These block attributes are not properly sanitized. The usage of wp_kses_post on the $attributes['alt'] also cannot prevent the double quotes from breaking out from the image alt attribute. For the render_bg_image function, the vulnerable variable is on $styles.
The second vulnerable case comes from wp:woocommerce/mini-cart block. The vulnerable block attributes exist on get_markup, get_cart_price_markup, and get_include_tax_label_markup function. First, let’s view the get_markup function :
protected function get_markup( $attributes ) {
if ( is_admin() || WC()->is_rest_api_request() ) {
// In the editor we will display the placeholder, so no need to load
// real cart data and to print the markup.
return '';
}
$classes_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array( 'text_color', 'background_color', 'font_size', 'font_weight', 'font_family' ) );
$wrapper_classes = sprintf( 'wc-block-mini-cart wp-block-woocommerce-mini-cart %s', $classes_styles['classes'] );
if ( ! empty( $attributes['className'] ) ) {
$wrapper_classes .= ' ' . $attributes['className'];
}
$wrapper_styles = $classes_styles['styles'];
$icon_color = array_key_exists( 'iconColor', $attributes ) ? $attributes['iconColor']['color'] : 'currentColor';
$product_count_color = array_key_exists( 'productCountColor', $attributes ) ? $attributes['productCountColor']['color'] : '';
// Default "Cart" icon.
$icon = '';
if ( isset( $attributes['miniCartIcon'] ) ) {
if ( 'bag' === $attributes['miniCartIcon'] ) {
$icon = '';
} elseif ( 'bag-alt' === $attributes['miniCartIcon'] ) {
$icon = '';
}
}
$button_html = $this->get_cart_price_markup( $attributes ) . '
' . $icon . '
';
if ( is_cart() || is_checkout() ) {
if ( $this->should_not_render_mini_cart( $attributes ) ) {
return '';
}
// It is not necessary to load the Mini-Cart Block on Cart and Checkout page.
return '
';
}
----------------------- CUTTED HERE -----------------------
Notice that the vulnerable variable located on $icon_color, $product_count_color and $wrapper_classes which all come from block attributes and are not properly sanitized before concatenated as HTML value.
Let’s view the get_cart_price_markup and get_include_tax_label_markup function:
The vulnerable variable on those two functions is the same, which comes from $price_color variable constructed from $attributes['priceColor']['color'].
The third vulnerable case is on wp:woocommerce/product-image block and exist on render_on_sale_badge function :
The vulnerable variable is located on $attributes['saleBadgeAlign'] where it is directly concatenated inside of the div class attribute without proper escaping.
The fourth and last vulnerable case exists on wp:woocommerce/product-results-count block, specifically on render function:
The vulnerable parameter on this function is located on $classname which come from $attributes['className'] value.
Jetpack Contributor+ Cross-Site Scripting (XSS)
We also found several vulnerable cases on the custom Gutenberg blocks. The first vulnerable case exists on wp:jetpack/button block specifically on render_block function:
The vulnerable variable is located on $text which is constructed from the “text” attributes. The code already tried to sanitize the value using wp_strip_all_tags function which will make the HTML tag to be stripped, but it’s still not a proper sanitization since we could just break out of the double quotes to trigger the XSS.
The other vulnerable variable is located on $element which is constructed from the “element” attributes. This value will be constructed as the opening and closing HTML tags. With this condition, we could just specify a script tag with a custom src attribute to trigger the XSS.
The second vulnerable case comes from wp:jetpack/recurring-payments block, specifically on deprecated_render_button_v1 function:
The vulnerable variable located on $attrs['submitButtonAttributes'] at the end of the function where the variable is directly concatenated inside of a tag. Usage of sanitize_text_field function in this case is not useful since we could just specify an arbitrary attribute to trigger the XSS inside the a tag.
The third vulnerable case exists on wp:jetpack/story block. The vulnerable attribute is located on render_video function:
The vulnerable variable comes from $media['id'] which will be directly concatenated to the class attribute of the video tag without proper sanitization. The $media object itself is constructed from a specific block attribute.
The last vulnerable case exists in the wp:videopress/video block, specifically on render_videopress_video_block function:
public static function render_videopress_video_block( $block_attributes, $content ) {
global $wp_embed;
// CSS classes
$align = isset( $block_attributes['align'] ) ? $block_attributes['align'] : null;
$align_class = $align ? ' align' . $align : '';
$custom_class = isset( $block_attributes['className'] ) ? ' ' . $block_attributes['className'] : '';
$classes = 'wp-block-jetpack-videopress jetpack-videopress-player' . $custom_class . $align_class;
// Inline style
$style = '';
$max_width = isset( $block_attributes['maxWidth'] ) ? $block_attributes['maxWidth'] : null;
if ( $max_width && $max_width !== '100%' ) {
$style = sprintf( 'max-width: %s; margin: auto;', $max_width );
}
/*
* element
* Caption is stored into the block attributes,
* but also it was stored into the element,
* meaning that it could be stored in two different places.
*/
$figcaption = '';
// Caption from block attributes
$caption = isset( $block_attributes['caption'] ) ? $block_attributes['caption'] : null;
/*
* If the caption is not stored into the block attributes,
* try to get it from the element.
*/
if ( $caption === null ) {
preg_match( '/(.*?)/', $content, $matches );
$caption = isset( $matches[1] ) ? $matches[1] : null;
}
// If we have a caption, create the element.
if ( $caption !== null ) {
$figcaption = sprintf( '%s', wp_kses_post( $caption ) );
}
// Custom anchor from block content
$id_attribute = '';
// Try to get the custom anchor from the block attributes.
if ( isset( $block_attributes['anchor'] ) && $block_attributes['anchor'] ) {
$id_attribute = sprintf( 'id="%s"', $block_attributes['anchor'] );
} elseif ( preg_match( '/]*id="([^"]+)"/', $content, $matches ) ) {
// Othwerwise, try to get the custom anchor from the element.
$id_attribute = sprintf( 'id="%s"', $matches[1] );
}
// Preview On Hover data
$is_poh_enabled =
isset( $block_attributes['posterData']['previewOnHover'] ) &&
$block_attributes['posterData']['previewOnHover'];
$autoplay = isset( $block_attributes['autoplay'] ) ? $block_attributes['autoplay'] : false;
$controls = isset( $block_attributes['controls'] ) ? $block_attributes['controls'] : false;
$poster = isset( $block_attributes['posterData']['url'] ) ? $block_attributes['posterData']['url'] : null;
$preview_on_hover = '';
if ( $is_poh_enabled ) {
$preview_on_hover = array(
'previewAtTime' => $block_attributes['posterData']['previewAtTime'],
'previewLoopDuration' => $block_attributes['posterData']['previewLoopDuration'],
'autoplay' => $autoplay,
'showControls' => $controls,
);
// Create inlione style in case video has a custom poster.
$inline_style = '';
if ( $poster ) {
$inline_style = sprintf(
'style="background-image: url(%s); background-size: cover;
background-position: center center;"',
$poster
);
}
// Expose the preview on hover data to the client.
$preview_on_hover = sprintf(
'',
$inline_style,
wp_json_encode( $preview_on_hover )
);
// Set `autoplay` and `muted` attributes to the video element.
$block_attributes['autoplay'] = true;
$block_attributes['muted'] = true;
}
$figure_template = '
%4$s
%5$s
';
// VideoPress URL
$guid = isset( $block_attributes['guid'] ) ? $block_attributes['guid'] : null;
$videopress_url = Utils::get_video_press_url( $guid, $block_attributes );
$video_wrapper = '';
$video_wrapper_classes = 'jetpack-videopress-player__wrapper';
if ( $videopress_url ) {
$videopress_url = wp_kses_post( $videopress_url );
$oembed_html = apply_filters( 'video_embed_html', $wp_embed->shortcode( array(), $videopress_url ) );
$video_wrapper = sprintf(
'
The vulnerable variable is located on $id_attribute and $inline_style. The $id_attribute variable is constructed from $block_attributes['anchor'] and will be directly constructed to the id attribute. The $inline_style variable is constructed from $block_attributes['posterData']['url'].
The patch
Since the main issue of this vulnerability is related to escaping and sanitizing block attributes that are constructed inside of HTML tag, applying certain protection such as using esc_attr, esc_html, sanitize_key and other whitelist processes should be enough to patch all of the reported issues.
Usage of custom posts or page elements such as Gutenberg block and shortcode could make the content to be richer and easily modified. When developing a custom block or shortcode, we recommend putting more attention to all of the attribute’s process and display.
Generally, a possible XSS on block or shortcode elements can only achieved when the attribute is directly placed inside an HTML tag without proper escaping and sanitization.
Always use proper formatting and implement esc_attr function to the value of the attribute that is placed inside of double quotes of an HTML tag attribute parameter. For some edge cases when the attribute is used as an opening or closing HTML tag, we recommend applying a whitelist of the allowed HTML tags.
Timeline
18 September, 2023We found the vulnerability and reached out to the Automattic team.
19 September, 2023Jetpack version 12.6 released to patch the first 3 vulnerable cases.
03 October, 2023We found the additional 4th vulnerable case on Jetpack and reached out to the Automattic team.
13 October, 2023WooCommerce version 8.2.0 released to fully patched the reported issues.
23 October, 2023Jetpack version 12.8-a.3 released to fully patched the reported issues.