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 × %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 × %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 ) . ' '; ?>
- <?php echo apply_filters( 'woocommerce_checkout_cart_item_quantity', ' <strong class="product-quantity">' . sprintf( '× %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( '× %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.
*/