Commit 783a0b4cd2f for woocommerce
commit 783a0b4cd2f08a9d5f88a3393e41e7d5d8bc1f2a
Author: Alba Rincón <albarin@users.noreply.github.com>
Date: Tue Apr 14 09:51:22 2026 +0200
REST API: reject orders endpoint updates against non-shop_order IDs (#64050)
* REST API: reject orders endpoint updates against non-shop_order IDs
* Add PHPStan generic type to update_item param
* Remove accidentally tracked local PR description
* Add changefile(s) from automation for the following project(s): woocommerce
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/64050-fix-63936-orders-rest-type-guard b/plugins/woocommerce/changelog/64050-fix-63936-orders-rest-type-guard
new file mode 100644
index 00000000000..77f6af0c7b7
--- /dev/null
+++ b/plugins/woocommerce/changelog/64050-fix-63936-orders-rest-type-guard
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+REST API: prevent PUT /wc/v(2|3)/orders/{id} from converting non-shop_order records into orders.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
index 0f156c654f6..024b0566782 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
@@ -695,6 +695,25 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
return empty( $schema['readonly'] );
}
+ /**
+ * Update a single order.
+ *
+ * Rejects IDs whose underlying type isn't shop_order (e.g. shop_subscription) to avoid
+ * silently converting them on save. Mirrors the upfront type check already performed by
+ * WC_REST_Orders_V1_Controller::update_item().
+ *
+ * @param WP_REST_Request<array<string, mixed>> $request Full details about the request.
+ * @return WP_Error|WP_REST_Response
+ */
+ public function update_item( $request ) {
+ $id = (int) $request['id'];
+ if ( empty( $id ) || OrderUtil::get_order_type( $id ) !== $this->post_type ) {
+ return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
+ }
+
+ return parent::update_item( $request );
+ }
+
/**
* Prepare a single order for create or update.
*
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
index 4fcc546e9fa..58fea1ad66b 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
@@ -317,6 +317,70 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->assertEquals( 'rest-api', $order->get_created_via() );
}
+ /**
+ * PUT against an ID belonging to a non 'shop_order' post type must be rejected, never
+ * silently converted.
+ */
+ public function test_update_rejects_non_shop_order_post_type(): void {
+ wc_register_order_type( 'shop_test' );
+ $post_id = wp_insert_post(
+ array(
+ 'post_type' => 'shop_test',
+ 'post_status' => 'wc-pending',
+ 'post_title' => 'test',
+ )
+ );
+
+ $request = new \WP_REST_Request( 'PUT', '/wc/v3/orders/' . $post_id );
+ $request->set_body_params( array( 'customer_note' => 'should not apply' ) );
+
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 400, $response->get_status() );
+ $this->assertArrayHasKey( 'code', $data );
+ $this->assertSame( 'woocommerce_rest_shop_order_invalid_id', $data['code'] );
+
+ // The persisted record must be untouched: same post type, no added customer_note.
+ $this->assertSame( 'shop_test', get_post_type( $post_id ) );
+ $this->assertSame( '', (string) get_post_meta( $post_id, '_customer_note', true ) );
+
+ unregister_post_type( 'shop_test' );
+ }
+
+ /**
+ * PUT against a non-existent ID returns the standard invalid-id error.
+ */
+ public function test_update_rejects_nonexistent_id(): void {
+ $request = new \WP_REST_Request( 'PUT', '/wc/v3/orders/999999999' );
+ $request->set_body_params( array( 'customer_note' => 'irrelevant' ) );
+
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertSame( 400, $response->get_status() );
+ $this->assertArrayHasKey( 'code', $data );
+ $this->assertSame( 'woocommerce_rest_shop_order_invalid_id', $data['code'] );
+ }
+
+ /**
+ * The type-mismatch override must still delegate normal shop_order updates to the
+ * parent controller and apply the request body.
+ */
+ public function test_update_shop_order_passes_type_guard_and_applies_changes(): void {
+ $order = new \WC_Order();
+ $order->set_customer_note( 'before' );
+ $order->save();
+
+ $request = new \WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
+ $request->set_body_params( array( 'customer_note' => 'after' ) );
+
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertSame( 'after', wc_get_order( $order->get_id() )->get_customer_note() );
+ }
+
/**
* Tests that the created_via parameter cannot be updated.
*/