Commit eb5251c9ba7 for woocommerce

commit eb5251c9ba7fcacfef53650c8372975d6a02f5f7
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Wed Nov 4 14:08:18 2026 +0100

    Validate cart item quantity on the Store API update route (#70635)

    * Validate cart item quantity on the Store API update route

    PUT /wc/store/v1/cart/items/<key> now validates quantity against the schema and normalises it with wc_stock_amount(). The mini-cart and review-order templates escape the quantity.

    Co-authored-by: Jorge A. Torres <184724+jorgeatorres@users.noreply.github.com>

    * Keep the mini-cart template version at 11.4.0 on trunk

    * Revert "Keep the mini-cart template version at 11.4.0 on trunk"

    This reverts commit 9fd6839d180a88600ff055a74ab217e52044fcda.

    ---------

    Co-authored-by: Jorge A. Torres <184724+jorgeatorres@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/fix-store-api-cart-items-validate-quantity b/plugins/woocommerce/changelog/fix-store-api-cart-items-validate-quantity
new file mode 100644
index 00000000000..c5367f682ea
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-store-api-cart-items-validate-quantity
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Validate the quantity when updating a cart item via the Store API.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartItemsByKey.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartItemsByKey.php
index b472ca00baa..d63ba6751ba 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartItemsByKey.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartItemsByKey.php
@@ -64,7 +64,16 @@ class CartItemsByKey extends AbstractCartRoute {
 				'methods'             => \WP_REST_Server::EDITABLE,
 				'callback'            => array( $this, 'get_response' ),
 				'permission_callback' => '__return_true',
-				'args'                => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
+				'args'                => array_merge(
+					$this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
+					[
+						// Manually add quantity so it gets validated.
+						'quantity' => [
+							'description' => __( 'New quantity of the item in the cart.', 'woocommerce' ),
+							'type'        => 'number',
+						],
+					]
+				),
 			],
 			[
 				'methods'             => \WP_REST_Server::DELETABLE,
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
index 742a382fdf2..79ddd787e79 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
@@ -267,6 +267,8 @@ class CartController {
 			throw new RouteException( 'woocommerce_rest_cart_invalid_product', esc_html__( 'Cart item is invalid.', 'woocommerce' ), 404 );
 		}

+		// Normalize the raw value so it's properly stored.
+		$quantity            = wc_stock_amount( $quantity );
 		$quantity_validation = ( new QuantityLimits() )->validate_cart_item_quantity( $quantity, $cart_item );

 		if ( is_wp_error( $quantity_validation ) ) {
diff --git a/plugins/woocommerce/templates/cart/mini-cart.php b/plugins/woocommerce/templates/cart/mini-cart.php
index 598cff58c31..c218ff71b2e 100644
--- a/plugins/woocommerce/templates/cart/mini-cart.php
+++ b/plugins/woocommerce/templates/cart/mini-cart.php
@@ -14,7 +14,7 @@
  *
  * @see     https://woocommerce.com/document/template-structure/
  * @package WooCommerce\Templates
- * @version 11.2.0
+ * @version 11.2.2
  */

 defined( 'ABSPATH' ) || exit;
@@ -79,7 +79,7 @@ do_action( 'woocommerce_before_mini_cart' ); ?>
 						</a>
 					<?php endif; ?>
 					<?php echo wc_get_formatted_cart_item_data( $cart_item, false, $cart_item_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
-					<?php echo apply_filters( 'woocommerce_widget_cart_item_quantity', '<span class="quantity">' . sprintf( '%s &times; %s', $cart_item['quantity'], $product_price ) . '</span>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+					<?php echo apply_filters( 'woocommerce_widget_cart_item_quantity', '<span class="quantity">' . sprintf( '%s &times; %s', esc_html( $cart_item['quantity'] ), $product_price ) . '</span>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WooCommerce.Commenting.CommentHooks.MissingHookComment -- pre-existing undocumented hook, out of scope here ?>
 				</li>
 				<?php
 			}
diff --git a/plugins/woocommerce/templates/checkout/review-order.php b/plugins/woocommerce/templates/checkout/review-order.php
index 253496fd303..e03fb5ffbb6 100644
--- a/plugins/woocommerce/templates/checkout/review-order.php
+++ b/plugins/woocommerce/templates/checkout/review-order.php
@@ -55,7 +55,7 @@ defined( 'ABSPATH' ) || exit;
 						$product_name = apply_filters( 'woocommerce_cart_item_name', $cart_item_name, $cart_item, $cart_item_key );
 						?>
 						<?php echo wp_kses_post( $product_name ) . '&nbsp;'; ?>
-						<?php echo apply_filters( 'woocommerce_checkout_cart_item_quantity', ' <strong class="product-quantity">' . sprintf( '&times;&nbsp;%s', $cart_item['quantity'] ) . '</strong>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+						<?php echo apply_filters( 'woocommerce_checkout_cart_item_quantity', ' <strong class="product-quantity">' . sprintf( '&times;&nbsp;%s', esc_html( $cart_item['quantity'] ) ) . '</strong>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WooCommerce.Commenting.CommentHooks.MissingHookComment -- pre-existing undocumented hook, out of scope here ?>
 						<?php echo wc_get_formatted_cart_item_data( $cart_item, false, $cart_item_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 						<?php
 						/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
index 8ebb68bb26c..578105e6990 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
@@ -314,6 +314,22 @@ class CartItems extends ControllerTestCase {
 		$this->assertEquals( 10, $data['quantity'] );
 	}

+	/**
+	 * Test updating an item with a quantity that is not a number is rejected and leaves the cart unchanged.
+	 */
+	public function test_update_item_rejects_non_numeric_quantity() {
+		$request = new \WP_REST_Request( 'PUT', '/wc/store/v1/cart/items/' . $this->keys[0] );
+		$request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
+		$request->set_body_params(
+			array(
+				'quantity' => '1abc',
+			)
+		);
+
+		$this->assertAPIResponse( $request, 400, array( 'code' => 'rest_invalid_param' ) );
+		$this->assertSame( 2, wc()->cart->get_cart_item( $this->keys[0] )['quantity'] );
+	}
+
 	/**
 	 * Test delete item.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
index 475c4302f30..e274fd059e3 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
@@ -80,6 +80,27 @@ class CartControllerTests extends \WC_Unit_Test_Case {
 		wc()->cart->empty_cart();
 	}

+	/**
+	 * Test set_cart_item_quantity stores the normalized quantity that was validated, not the raw value.
+	 */
+	public function test_set_cart_item_quantity_stores_normalized_quantity() {
+		$class    = new CartController();
+		$fixtures = new FixtureData();
+
+		$product = $fixtures->get_simple_product(
+			array(
+				'name'          => 'Test Product 1',
+				'regular_price' => 10,
+			)
+		);
+
+		$product_key = wc()->cart->add_to_cart( $product->get_id(), 1 );
+		$class->set_cart_item_quantity( $product_key, '3abc' );
+
+		$this->assertSame( 3, wc()->cart->get_cart_item( $product_key )['quantity'] );
+		wc()->cart->empty_cart();
+	}
+
 	/**
 	 * Test cart error code is getting exposed.
 	 */