Commit 7d1e5e233e5 for woocommerce

commit 7d1e5e233e53decdb047e01f8c0263f18d15d9f4
Author: Liam Sarsfield <43409125+LiamSarsfield@users.noreply.github.com>
Date:   Mon Aug 10 14:22:41 2026 +0100

    Update external product button label handling (#67538)

    Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/fix-WOO6-78 b/plugins/woocommerce/changelog/fix-WOO6-78
new file mode 100644
index 00000000000..9fe6d488c8f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-WOO6-78
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Update product button label checks.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php
index d742f7c2d87..143ef5dd4d4 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php
@@ -1354,7 +1354,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller {
 			}

 			if ( isset( $request['button_text'] ) ) {
-				$product->set_button_text( $request['button_text'] );
+				$product->set_button_text( sanitize_text_field( $request['button_text'] ) );
 			}
 		}

diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
index 66d114d0f59..17b9f0ed43c 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
@@ -1416,7 +1416,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
 			}

 			if ( isset( $request['button_text'] ) ) {
-				$product->set_button_text( $request['button_text'] );
+				$product->set_button_text( sanitize_text_field( $request['button_text'] ) );
 			}
 		}

diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index cd2e44c5207..fc3034341c2 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -1125,7 +1125,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
 			}

 			if ( isset( $request['button_text'] ) ) {
-				$product->set_button_text( $request['button_text'] );
+				$product->set_button_text( sanitize_text_field( $request['button_text'] ) );
 			}
 		}

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
index e865ec849f4..29c1f13a92b 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
@@ -296,7 +296,7 @@ class ProductButton extends AbstractBlock {
 				'{context_directives}'     => $context_directives,
 				'{button_styles}'          => esc_attr( $styles_and_classes['styles'] ),
 				'{attributes}'             => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
-				'{add_to_cart_text}'       => $is_ajax_button ? '' : $add_to_cart_text,
+				'{add_to_cart_text}'       => $is_ajax_button ? '' : esc_html( $add_to_cart_text ),
 				'{div_directives}'         => $is_ajax_button ? $div_directives : '',
 				'{button_directives}'      => $is_ajax_button ? $button_directives : $anchor_directive,
 				'{span_button_directives}' => $is_ajax_button ? $span_button_directives : '',
diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
index 31c7a09f5b3..d4bcdeb2f88 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -1397,7 +1397,7 @@ class Controller extends WC_REST_Products_V2_Controller {
 			}

 			if ( isset( $request['button_text'] ) ) {
-				$product->set_button_text( $request['button_text'] );
+				$product->set_button_text( sanitize_text_field( $request['button_text'] ) );
 			}
 		}

diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
index f20b9ea1e34..e56ed02e009 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
@@ -1945,6 +1945,28 @@ class WC_REST_Products_Controller_Tests extends WC_Unit_Test_Case {
 		$this->assertEquals( 200, $response->get_status() );
 	}

+	/**
+	 * @testdox Should sanitize external product button text.
+	 */
+	public function test_update_external_product_sanitizes_button_text(): void {
+		$shop_manager = self::factory()->user->create( array( 'role' => 'shop_manager' ) );
+		wp_set_current_user( $shop_manager );
+
+		$product = WC_Helper_Product::create_external_product();
+		$this->update_product_via_post_request(
+			$product,
+			array(
+				'button_text' => 'Buy now<style>.hidden { display: none; }</style>',
+			)
+		);
+
+		$updated_product = wc_get_product( $product->get_id() );
+
+		$this->assertSame( 'Buy now', $updated_product->get_button_text(), 'HTML should be removed from the button text.' );
+
+		$product->delete( true );
+	}
+
 	/**
 	 * Test that batch create operations update term counts correctly.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductButtonTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductButtonTest.php
new file mode 100644
index 00000000000..c7d0fea7266
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductButtonTest.php
@@ -0,0 +1,34 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the ProductButton block type.
+ */
+class ProductButtonTest extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Should escape an external product's button text.
+	 */
+	public function test_render_escapes_external_product_button_text(): void {
+		$product = WC_Helper_Product::create_external_product();
+		$product->set_button_text( 'Buy now<style>.hidden { display: none; }</style>' );
+		$product->save();
+
+		$markup = do_blocks(
+			'<!-- wp:woocommerce/single-product {"productId":' . $product->get_id() . '} -->' .
+			'<!-- wp:woocommerce/product-button /-->' .
+			'<!-- /wp:woocommerce/single-product -->'
+		);
+
+		$this->assertStringContainsString( 'Buy now&lt;style&gt;.hidden { display: none; }&lt;/style&gt;', $markup, 'The button text should be escaped.' );
+		$this->assertStringNotContainsString( 'Buy now<style>.hidden { display: none; }</style>', $markup, 'The button text should not render as HTML.' );
+
+		$product->delete( true );
+	}
+}