Commit 0ca5eb4d4b1 for woocommerce

commit 0ca5eb4d4b13664e10065a82f02ef7128b24b598
Author: Francesco <frosso@users.noreply.github.com>
Date:   Fri Oct 2 11:13:27 2026 +0200

    fix: stop Store API extension data leaking between namespaces (#68397)

diff --git a/plugins/woocommerce/changelog/fix-store-api-extension-data-leak b/plugins/woocommerce/changelog/fix-store-api-extension-data-leak
new file mode 100644
index 00000000000..dcf91c4c7d8
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-store-api-extension-data-leak
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Store API: stop an extension whose data or schema callback throws from receiving another extension's data
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 16080aab306..61afbc31b7a 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -68160,18 +68160,6 @@ parameters:
 			count: 1
 			path: src/StoreApi/Schemas/ExtendSchema.php

-		-
-			message: '#^Variable \$data might not be defined\.$#'
-			identifier: variable.undefined
-			count: 1
-			path: src/StoreApi/Schemas/ExtendSchema.php
-
-		-
-			message: '#^Variable \$schema might not be defined\.$#'
-			identifier: variable.undefined
-			count: 1
-			path: src/StoreApi/Schemas/ExtendSchema.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\AbstractAddressSchema\:\:sanitize_callback\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
 			identifier: missingType.generics
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/ExtendSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/ExtendSchema.php
index 49be2891e5e..7df077560e9 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/ExtendSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/ExtendSchema.php
@@ -225,6 +225,10 @@ final class ExtendSchema {
 				if ( is_null( $callbacks['data_callback'] ) ) {
 					continue;
 				}
+
+				// A callback that throws leaves $data untouched, so reset it or this namespace inherits the previous one's data.
+				$data = [];
+
 				try {
 					$data = $callbacks['data_callback']( ...$passed_args );

@@ -259,6 +263,9 @@ final class ExtendSchema {
 				if ( is_null( $callbacks['schema_callback'] ) ) {
 					continue;
 				}
+
+				$schema = [];
+
 				try {
 					$schema = $callbacks['schema_callback']( ...$passed_args );

diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/ExtendSchemaTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/ExtendSchemaTests.php
index 72517db5ace..dfffe9652be 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/ExtendSchemaTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/ExtendSchemaTests.php
@@ -41,6 +41,9 @@ class ExtendSchemaTests extends TestCase {
 		$this->dummy       = function () {
 			return null;
 		};
+
+		// ExtendSchema only rethrows callback errors for admins with WP_DEBUG on; logged out is the production path.
+		wp_set_current_user( 0 );
 	}

 	/**
@@ -97,4 +100,118 @@ class ExtendSchemaTests extends TestCase {
 		);
 		$this->mock_extend->get_update_callback( 'nonexistent-plugin' );
 	}
+
+	/**
+	 * @testdox A namespace whose data callback throws gets an empty payload, not the previous namespace's data.
+	 */
+	public function test_get_endpoint_data_does_not_leak_data_between_namespaces() {
+		$this->register_endpoint_data(
+			'first-plugin',
+			function () {
+				return array( 'token' => 'abc' );
+			}
+		);
+		$this->register_endpoint_data(
+			'second-plugin',
+			function () {
+				throw new \Exception( 'Callback failed.' );
+			}
+		);
+
+		$data = $this->mock_extend->get_endpoint_data( 'cart' );
+
+		$this->assertSame( array( 'token' => 'abc' ), $data->{'first-plugin'} );
+		$this->assertSame( array(), $data->{'second-plugin'}, 'A failed namespace must not receive another namespace\'s data' );
+	}
+
+	/**
+	 * @testdox The first namespace gets an empty payload when its data callback throws.
+	 */
+	public function test_get_endpoint_data_when_the_first_namespace_throws() {
+		$this->register_endpoint_data(
+			'first-plugin',
+			function () {
+				throw new \Exception( 'Callback failed.' );
+			}
+		);
+		$this->register_endpoint_data(
+			'second-plugin',
+			function () {
+				return array( 'token' => 'abc' );
+			}
+		);
+
+		$data = $this->mock_extend->get_endpoint_data( 'cart' );
+
+		$this->assertSame( array(), $data->{'first-plugin'} );
+		$this->assertSame( array( 'token' => 'abc' ), $data->{'second-plugin'} );
+	}
+
+	/**
+	 * @testdox A namespace whose schema callback throws gets empty properties, not the previous namespace's schema.
+	 */
+	public function test_get_endpoint_schema_does_not_leak_schema_between_namespaces() {
+		$this->register_endpoint_data(
+			'first-plugin',
+			null,
+			function () {
+				return array( 'token' => array( 'type' => 'string' ) );
+			}
+		);
+		$this->register_endpoint_data(
+			'second-plugin',
+			null,
+			function () {
+				throw new \Exception( 'Callback failed.' );
+			}
+		);
+
+		$schema = $this->mock_extend->get_endpoint_schema( 'cart' );
+
+		$this->assertSame( array( 'token' => array( 'type' => 'string' ) ), $schema->{'first-plugin'}['properties'] );
+		$this->assertSame( array(), $schema->{'second-plugin'}['properties'], 'A failed namespace must not receive another namespace\'s schema' );
+	}
+
+	/**
+	 * @testdox The first namespace gets empty properties when its schema callback throws.
+	 */
+	public function test_get_endpoint_schema_when_the_first_namespace_throws() {
+		$this->register_endpoint_data(
+			'first-plugin',
+			null,
+			function () {
+				throw new \Exception( 'Callback failed.' );
+			}
+		);
+		$this->register_endpoint_data(
+			'second-plugin',
+			null,
+			function () {
+				return array( 'token' => array( 'type' => 'string' ) );
+			}
+		);
+
+		$schema = $this->mock_extend->get_endpoint_schema( 'cart' );
+
+		$this->assertSame( array(), $schema->{'first-plugin'}['properties'] );
+		$this->assertSame( array( 'token' => array( 'type' => 'string' ) ), $schema->{'second-plugin'}['properties'] );
+	}
+
+	/**
+	 * Registers cart endpoint data for a namespace.
+	 *
+	 * @param string        $plugin_namespace Plugin namespace.
+	 * @param callable|null $data_callback    Callback returning endpoint data.
+	 * @param callable|null $schema_callback  Callback returning endpoint schema.
+	 */
+	private function register_endpoint_data( $plugin_namespace, $data_callback = null, $schema_callback = null ) {
+		$this->mock_extend->register_endpoint_data(
+			array(
+				'endpoint'        => 'cart',
+				'namespace'       => $plugin_namespace,
+				'data_callback'   => $data_callback,
+				'schema_callback' => $schema_callback,
+			)
+		);
+	}
 }