Authenticated Stored XSS in WooCommerce and Jetpack Plugin

Published 15 November 2023
Updated 6 December 2023
Table of Contents

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.

You can sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.

For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.

About the WooCommerce and Jetpack plugin

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.

WooCommerce Contributor+ Cross-Site Scripting (XSS)

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:

private function render_image( $attributes, $item, string $image_url ) {
    $style = sprintf( 'object-fit: %s;', $attributes['imageFit'] );

    if ( $this->hasFocalPoint( $attributes ) ) {
        $style .= sprintf(
            'object-position: %s%% %s%%;',
            $attributes['focalPoint']['x'] * 100,
            $attributes['focalPoint']['y'] * 100
        );
    }

    if ( ! empty( $image_url ) ) {
        return sprintf(
            '<img alt="%1$s" class="wc-block-%2$s__background-image" src="%3$s" style="%4$s" />',
            wp_kses_post( $attributes['alt'] ?: $this->get_item_title( $item ) ),
            $this->block_name,
            $image_url,
            $style
        );
    }

    return '';
}

private function render_bg_image( $attributes, $image_url ) {
    $styles = $this->get_bg_styles( $attributes, $image_url );

    $classes = [ "wc-block-{$this->block_name}__background-image" ];

    if ( $attributes['hasParallax'] ) {
        $classes[] = ' has-parallax';
    }

    return sprintf( '<div class="%1$s" style="%2$s" /></div>', implode( ' ', $classes ), $styles );
}

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 = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="' . $icon_color . '" xmlns="http://www.w3.org/2000/svg">
                <circle cx="12.6667" cy="24.6667" r="2" fill="' . $icon_color . '"/>
                <circle cx="23.3333" cy="24.6667" r="2" fill="' . $icon_color . '"/>
                <path fill-rule="evenodd" clip-rule="evenodd" d="M9.28491 10.0356C9.47481 9.80216 9.75971 9.66667 10.0606 9.66667H25.3333C25.6232 9.66667 25.8989 9.79247 26.0888 10.0115C26.2787 10.2305 26.3643 10.5211 26.3233 10.8081L24.99 20.1414C24.9196 20.6341 24.4977 21 24 21H12C11.5261 21 11.1173 20.6674 11.0209 20.2034L9.08153 10.8701C9.02031 10.5755 9.09501 10.269 9.28491 10.0356ZM11.2898 11.6667L12.8136 19H23.1327L24.1803 11.6667H11.2898Z" fill="' . $icon_color . '"/>
                <path fill-rule="evenodd" clip-rule="evenodd" d="M5.66669 6.66667C5.66669 6.11438 6.1144 5.66667 6.66669 5.66667H9.33335C9.81664 5.66667 10.2308 6.01229 10.3172 6.48778L11.0445 10.4878C11.1433 11.0312 10.7829 11.5517 10.2395 11.6505C9.69614 11.7493 9.17555 11.3889 9.07676 10.8456L8.49878 7.66667H6.66669C6.1144 7.66667 5.66669 7.21895 5.66669 6.66667Z" fill="' . $icon_color . '"/>
            </svg>';

    if ( isset( $attributes['miniCartIcon'] ) ) {
        if ( 'bag' === $attributes['miniCartIcon'] ) {
            $icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M12.4444 14.2222C12.9354 14.2222 13.3333 14.6202 13.3333 15.1111C13.3333 15.8183 13.6143 16.4966 14.1144 16.9967C14.6145 17.4968 15.2927 17.7778 16 17.7778C16.7072 17.7778 17.3855 17.4968 17.8856 16.9967C18.3857 16.4966 18.6667 15.8183 18.6667 15.1111C18.6667 14.6202 19.0646 14.2222 19.5555 14.2222C20.0465 14.2222 20.4444 14.6202 20.4444 15.1111C20.4444 16.2898 19.9762 17.4203 19.1427 18.2538C18.3092 19.0873 17.1787 19.5555 16 19.5555C14.8212 19.5555 13.6908 19.0873 12.8573 18.2538C12.0238 17.4203 11.5555 16.2898 11.5555 15.1111C11.5555 14.6202 11.9535 14.2222 12.4444 14.2222Z" fill="' . $icon_color . '""/>
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M11.2408 6.68254C11.4307 6.46089 11.7081 6.33333 12 6.33333H20C20.2919 6.33333 20.5693 6.46089 20.7593 6.68254L24.7593 11.3492C25.0134 11.6457 25.0717 12.0631 24.9085 12.4179C24.7453 12.7727 24.3905 13 24 13H8.00001C7.60948 13 7.25469 12.7727 7.0915 12.4179C6.92832 12.0631 6.9866 11.6457 7.24076 11.3492L11.2408 6.68254ZM12.4599 8.33333L10.1742 11H21.8258L19.5401 8.33333H12.4599Z" fill="' . $icon_color . '"/>
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M7 12C7 11.4477 7.44772 11 8 11H24C24.5523 11 25 11.4477 25 12V25.3333C25 25.8856 24.5523 26.3333 24 26.3333H8C7.44772 26.3333 7 25.8856 7 25.3333V12ZM9 13V24.3333H23V13H9Z" fill="' . $icon_color . '"/>
                    </svg>';
        } elseif ( 'bag-alt' === $attributes['miniCartIcon'] ) {
            $icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M19.5556 12.3333C19.0646 12.3333 18.6667 11.9354 18.6667 11.4444C18.6667 10.7372 18.3857 8.05893 17.8856 7.55883C17.3855 7.05873 16.7073 6.77778 16 6.77778C15.2928 6.77778 14.6145 7.05873 14.1144 7.55883C13.6143 8.05893 13.3333 10.7372 13.3333 11.4444C13.3333 11.9354 12.9354 12.3333 12.4445 12.3333C11.9535 12.3333 11.5556 11.9354 11.5556 11.4444C11.5556 10.2657 12.0238 7.13524 12.8573 6.30175C13.6908 5.46825 14.8213 5 16 5C17.1788 5 18.3092 5.46825 19.1427 6.30175C19.9762 7.13524 20.4445 10.2657 20.4445 11.4444C20.4445 11.9354 20.0465 12.3333 19.5556 12.3333Z" fill="' . $icon_color . '"/>
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 12C7.5 11.4477 7.94772 11 8.5 11H23.5C24.0523 11 24.5 11.4477 24.5 12V25.3333C24.5 25.8856 24.0523 26.3333 23.5 26.3333H8.5C7.94772 26.3333 7.5 25.8856 7.5 25.3333V12ZM9.5 13V24.3333H22.5V13H9.5Z" fill="' . $icon_color . '" />
                    </svg>';
        }
    }

    $button_html = $this->get_cart_price_markup( $attributes ) . '
    <span class="wc-block-mini-cart__quantity-badge">
        ' . $icon . '
        <span class="wc-block-mini-cart__badge" style="background:' . $product_count_color . '"></span>
    </span>';

    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 '<div class="' . $wrapper_classes . '" style="visibility:hidden" aria-hidden="true">
            <button class="wc-block-mini-cart__button" disabled>' . $button_html . '</button>
        </div>';
    }
----------------------- 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:

/**
 * Returns the markup for the cart price.
 *
 * @param array $attributes Block attributes.
 *
 * @return string
 */
protected function get_cart_price_markup( $attributes ) {
    if ( isset( $attributes['hasHiddenPrice'] ) && false !== $attributes['hasHiddenPrice'] ) {
        return;
    }
    $price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';

    return '<span class="wc-block-mini-cart__amount" style="color:' . $price_color . ' "></span>' . $this->get_include_tax_label_markup( $attributes );
}

/**
 * Returns the markup for render the tax label.
 *
 * @param array $attributes Block attributes.
 *
 * @return string
 */
protected function get_include_tax_label_markup( $attributes ) {
    if ( empty( $this->tax_label ) ) {
        return '';
    }
    $price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';

    return '<small class="wc-block-mini-cart__tax-label" style="color:' . $price_color . ' " hidden>' . esc_html( $this->tax_label ) . '</small>';
}

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 :

private function render_on_sale_badge( $product, $attributes ) {
    if ( ! $product->is_on_sale() || false === $attributes['showSaleBadge'] ) {
        return '';
    }

    $font_size = StyleAttributesUtils::get_font_size_class_and_style( $attributes );

    $on_sale_badge = sprintf(
        '
        <div class="wc-block-components-product-sale-badge wc-block-components-product-sale-badge--align-%s wc-block-grid__product-onsale %s" style="%s">
            <span aria-hidden="true">%s</span>
            <span class="screen-reader-text">Product on sale</span>
        </div>
        ',
        $attributes['saleBadgeAlign'],
        isset( $font_size['class'] ) ? esc_attr( $font_size['class'] ) : '',
        isset( $font_size['style'] ) ? esc_attr( $font_size['style'] ) : '',
        esc_html__( 'Sale', 'woocommerce' )
    );
    return $on_sale_badge;
}

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:

protected function render( $attributes, $content, $block ) {
    ob_start();
    woocommerce_result_count();
    $product_results_count = ob_get_clean();

    $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
    $classname          = isset( $attributes['className'] ) ? $attributes['className'] : '';

    return sprintf(
        '<div class="woocommerce wc-block-product-results-count wp-block-woocommerce-product-results-count %1$s %2$s" style="%3$s">%4$s</div>',
        esc_attr( $classes_and_styles['classes'] ),
        $classname,
        esc_attr( $classes_and_styles['styles'] ),
        $product_results_count
    );
}

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:

function render_block( $attributes, $content ) {
	$save_in_post_content = get_attribute( $attributes, 'saveInPostContent' );

	// The Jetpack Button block depends on the core button block styles.
	// The following ensures that those styles are enqueued when rendering this block.
	enqueue_existing_button_style_dependency( 'core/button' );
	enqueue_existing_button_style_dependency( 'core/buttons' );

	Jetpack_Gutenberg::load_styles_as_required( FEATURE_NAME );

	if ( $save_in_post_content || ! class_exists( 'DOMDocument' ) ) {
		return $content;
	}

	$element   = get_attribute( $attributes, 'element' );
	$text      = get_attribute( $attributes, 'text' );
	$unique_id = get_attribute( $attributes, 'uniqueId' );
	$url       = get_attribute( $attributes, 'url' );
	$classes   = Blocks::classes( FEATURE_NAME, $attributes, array( 'wp-block-button' ) );

	$button_classes = get_button_classes( $attributes );
	$button_styles  = get_button_styles( $attributes );
	$wrapper_styles = get_button_wrapper_styles( $attributes );

	$wrapper_attributes = sprintf( ' class="%s" style="%s"', esc_attr( $classes ), esc_attr( $wrapper_styles ) );
	$button_attributes  = sprintf( ' class="%s" style="%s"', esc_attr( $button_classes ), esc_attr( $button_styles ) );

	if ( empty( $unique_id ) ) {
		$button_attributes .= ' data-id-attr="placeholder"';
	} else {
		$button_attributes .= sprintf( ' data-id-attr="%1$s" id="%1$s"', esc_attr( $unique_id ) );
	}

	if ( 'a' === $element ) {
		$button_attributes .= sprintf( ' href="%s" target="_blank" role="button" rel="noopener noreferrer"', esc_url( $url ) );
	} elseif ( 'button' === $element ) {
		$button_attributes .= ' type="submit"';
	} elseif ( 'input' === $element ) {
		$button_attributes .= sprintf( ' type="submit" value="%s"', wp_strip_all_tags( $text, true ) );
	}

	$button = 'input' === $element
		? '<' . $element . $button_attributes . ' />'
		: '<' . $element . $button_attributes . '>' . $text . '</' . $element . '>';

	// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	return '<div' . $wrapper_attributes . '>' . $button . '</div>';
}

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:

public function deprecated_render_button_v1( $attrs, $plan_id ) {
    $button_label = isset( $attrs['submitButtonText'] )
        ? $attrs['submitButtonText']
        : __( 'Your contribution', 'jetpack' );

    $button_styles = array();
    if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
        array_push(
            $button_styles,
            sprintf(
                'background-color: %s',
                sanitize_hex_color( $attrs['customBackgroundButtonColor'] )
            )
        );
    }
    if ( ! empty( $attrs['customTextButtonColor'] ) ) {
        array_push(
            $button_styles,
            sprintf(
                'color: %s',
                sanitize_hex_color( $attrs['customTextButtonColor'] )
            )
        );
    }
    $button_styles = implode( ';', $button_styles );

    return sprintf(
        '<div class="%1$s"><a role="button" %6$s href="%2$s" class="%3$s" style="%4$s">%5$s</a></div>',
        esc_attr(
            Blocks::classes(
                self::$button_block_name,
                $attrs,
                array( 'wp-block-button' )
            )
        ),
        esc_url( $this->get_subscription_url( $plan_id ) ),
        isset( $attrs['submitButtonClasses'] ) ? esc_attr( $attrs['submitButtonClasses'] ) : 'wp-block-button__link',
        esc_attr( $button_styles ),
        wp_kses( $button_label, self::$tags_allowed_in_the_button ),
        isset( $attrs['submitButtonAttributes'] ) ? sanitize_text_field( $attrs['submitButtonAttributes'] ) : '' // Needed for arbitrary target=_blank on WPCOM VIP.
    );
}

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:

function render_video( $media ) {
	if ( empty( $media['id'] ) || empty( $media['mime'] ) || empty( $media['url'] ) ) {
		return __( 'Error retrieving media', 'jetpack' );
	}

	if ( ! empty( $media['poster'] ) ) {
		return render_image(
			array_merge(
				$media,
				array(
					'type' => 'image',
					'url'  => $media['poster'],
				)
			)
		);
	}

	return sprintf(
		'<video
			title="%1$s"
			type="%2$s"
			class="wp-story-video intrinsic-ignore wp-video-%3$s"
			data-id="%3$s"
			src="%4$s">
		</video>',
		esc_attr( get_the_title( $media['id'] ) ),
		esc_attr( $media['mime'] ),
		$media['id'],
		esc_attr( $media['url'] )
	);
}

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 );
    }

    /*
     * <figcaption /> element
     * Caption is stored into the block attributes,
     * but also it was stored into the <figcaption /> 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 <figcaption /> element.
     */
    if ( $caption === null ) {
        preg_match( '/<figcaption>(.*?)<\/figcaption>/', $content, $matches );
        $caption = isset( $matches[1] ) ? $matches[1] : null;
    }

    // If we have a caption, create the <figcaption /> element.
    if ( $caption !== null ) {
        $figcaption = sprintf( '<figcaption>%s</figcaption>', 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( '/<figure[^>]*id="([^"]+)"/', $content, $matches ) ) {
        // Othwerwise, try to get the custom anchor from the <figure /> 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(
            '<div class="jetpack-videopress-player__overlay" %s></div><script type="application/json">%s</script>',
            $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 = '
    <figure class="%1$s" style="%2$s" %3$s>
        %4$s
        %5$s
    </figure>
    ';

    // 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(
            '<div class="%s">%s %s</div>',
            $video_wrapper_classes,
            $preview_on_hover,
            $oembed_html
        );
    }

    return sprintf(
        $figure_template,
        esc_attr( $classes ),
        esc_attr( $style ),
        $id_attribute,
        $video_wrapper,
        $figcaption
    );
}

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.

For WooCommerce, the patch can be seen below:
Patch case 1
Patch case 2
Patch case 3
Patch case 4

For Jetpack, the patch can be seen below:
Patch case 1
Patch case 2
Patch case 3
Patch case 4

Conclusion

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.
15 November, 2023Added the vulnerabilities to the Patchstack vulnerability database. Security advisory article publicly released.

Help us make the Internet a safer place

Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.

  • If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
  • If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.

The latest in Security advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu