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