Commit 5074b77c692 for woocommerce

commit 5074b77c692681463a947d63bc9b49f0fc4c8e86
Author: Lucio Giannotta <lucio.giannotta@a8c.com>
Date:   Tue Apr 14 09:37:15 2026 +0800

    Strip sensitive fields from V4 product response for low-privilege users (#63895)

    * Strip sensitive fields from V4 product response for low-privilege users

    The V4 Products Controller allows users with edit_posts (e.g. Authors)
    to view published products so Product blocks render in the editor.
    However, the full response included sensitive fields like download URLs,
    COGS data, and purchase notes that should only be visible to shop
    managers and admins.

    Add a capability check in prepare_object_for_response_core() that strips
    sensitive fields when the user lacks read_private_products. A
    SENSITIVE_FIELDS constant provides a single, inspectable list of the
    filtered fields.

    * Add `meta_data` to sensitive fields

    * Add changefile(s) from automation for the following project(s): woocommerce

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Tung Du <dinhtungdu@gmail.com>

diff --git a/plugins/woocommerce/changelog/63895-woo6-38-codex-review-v4-product-endpoint-now-exposes-sensitive-data b/plugins/woocommerce/changelog/63895-woo6-38-codex-review-v4-product-endpoint-now-exposes-sensitive-data
new file mode 100644
index 00000000000..97f07ab8508
--- /dev/null
+++ b/plugins/woocommerce/changelog/63895-woo6-38-codex-review-v4-product-endpoint-now-exposes-sensitive-data
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Strip sensitive fields (downloads, COGS, purchase note) from V4 products REST API response for users without product management capabilities.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/fix-v4-products-sensitive-field-exposure b/plugins/woocommerce/changelog/fix-v4-products-sensitive-field-exposure
new file mode 100644
index 00000000000..3577ca3d07c
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-v4-products-sensitive-field-exposure
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Strip sensitive fields (downloads, COGS, purchase note) from V4 products REST API response for users without product management capabilities.
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 c5f06a6aaff..ff2cb711175 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -44,6 +44,21 @@ class Controller extends WC_REST_Products_V2_Controller {

 	use CogsAwareRestControllerTrait;

+	/**
+	 * Fields stripped from the response for users without product management capabilities
+	 * (e.g. authors who can view published products via the edit_posts fallback).
+	 *
+	 * @since 10.8.0
+	 */
+	private const SENSITIVE_FIELDS = array(
+		'cost_of_goods_sold',
+		'downloads',
+		'download_limit',
+		'download_expiry',
+		'meta_data',
+		'purchase_note',
+	);
+
 	/**
 	 * Endpoint namespace.
 	 *
@@ -2277,6 +2292,14 @@ class Controller extends WC_REST_Products_V2_Controller {
 			'text'        => $object_data->add_to_cart_text(),
 			'single_text' => $object_data->single_add_to_cart_text(),
 		);
+
+		$post_type_object = get_post_type_object( 'product' );
+		if ( $post_type_object instanceof \WP_Post_Type && ! current_user_can( $post_type_object->cap->read_private_posts ) ) {
+			foreach ( self::SENSITIVE_FIELDS as $field ) {
+				unset( $data[ $field ] );
+			}
+		}
+
 		return $data;
 	}

diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php
index 3d05348c015..9be486ee79a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php
@@ -2126,4 +2126,116 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
 		$this->assertEquals( 403, $response->get_status() );
 		$this->assertEquals( 'woocommerce_rest_cannot_view', $response->get_data()['code'] );
 	}
+
+	/**
+	 * @testdox Should strip sensitive fields from response when author views a published product.
+	 */
+	public function test_get_published_product_as_author_strips_sensitive_fields(): void {
+		$download = new \WC_Product_Download();
+		$download->set_file( 'https://example.com/secret-file.zip' );
+		$download->set_name( 'Secret File' );
+
+		$product = WC_Helper_Product::create_simple_product(
+			true,
+			array(
+				'name'            => 'Downloadable Product',
+				'status'          => ProductStatus::PUBLISH,
+				'downloadable'    => true,
+				'download_limit'  => 5,
+				'download_expiry' => 30,
+				'purchase_note'   => 'Internal note for customers',
+			)
+		);
+		$product->set_downloads( array( $download ) );
+		$product->save();
+
+		$author = $this->factory->user->create(
+			array(
+				'role' => 'author',
+			)
+		);
+		wp_set_current_user( $author );
+
+		$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertArrayNotHasKey( 'downloads', $data, 'Author should not see download URLs' );
+		$this->assertArrayNotHasKey( 'download_limit', $data, 'Author should not see download limit' );
+		$this->assertArrayNotHasKey( 'download_expiry', $data, 'Author should not see download expiry' );
+		$this->assertArrayNotHasKey( 'purchase_note', $data, 'Author should not see purchase note' );
+	}
+
+	/**
+	 * @testdox Should strip COGS data from response when author views a published product.
+	 */
+	public function test_get_published_product_as_author_strips_cogs_data(): void {
+		$this->enable_cogs_feature();
+
+		$product = WC_Helper_Product::create_simple_product(
+			true,
+			array(
+				'name'   => 'Product With COGS',
+				'status' => ProductStatus::PUBLISH,
+			)
+		);
+		$product->set_cogs_value( 5.00 );
+		$product->save();
+
+		$author = $this->factory->user->create(
+			array(
+				'role' => 'author',
+			)
+		);
+		wp_set_current_user( $author );
+
+		$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertArrayNotHasKey( 'cost_of_goods_sold', $data, 'Author should not see COGS data' );
+	}
+
+	/**
+	 * @testdox Should include sensitive fields in response when shop manager views a product.
+	 */
+	public function test_get_product_as_shop_manager_includes_sensitive_fields(): void {
+		$this->enable_cogs_feature();
+
+		$download = new \WC_Product_Download();
+		$download->set_file( 'https://example.com/secret-file.zip' );
+		$download->set_name( 'Secret File' );
+
+		$product = WC_Helper_Product::create_simple_product(
+			true,
+			array(
+				'name'            => 'Full Access Product',
+				'status'          => ProductStatus::PUBLISH,
+				'downloadable'    => true,
+				'download_limit'  => 5,
+				'download_expiry' => 30,
+				'purchase_note'   => 'Internal note for customers',
+			)
+		);
+		$product->set_downloads( array( $download ) );
+		$product->set_cogs_value( 5.00 );
+		$product->save();
+
+		$shop_manager = $this->factory->user->create(
+			array(
+				'role' => 'shop_manager',
+			)
+		);
+		wp_set_current_user( $shop_manager );
+
+		$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) );
+		$data     = $response->get_data();
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertArrayHasKey( 'downloads', $data, 'Shop manager should see downloads' );
+		$this->assertArrayHasKey( 'download_limit', $data, 'Shop manager should see download limit' );
+		$this->assertArrayHasKey( 'download_expiry', $data, 'Shop manager should see download expiry' );
+		$this->assertArrayHasKey( 'purchase_note', $data, 'Shop manager should see purchase note' );
+		$this->assertArrayHasKey( 'cost_of_goods_sold', $data, 'Shop manager should see COGS data' );
+	}
 }