Commit e411ddb8b17 for woocommerce
commit e411ddb8b175a98cf9154f3807dff9e2545d013f
Author: Mike Jolley <mike.jolley@me.com>
Date: Mon Jun 29 10:42:59 2026 +0100
Fix backslashes stripped from Agentic Checkout buyer and address fields (#65900)
* Fix backslashes stripped from Agentic Checkout and Store API field validation
* Add changelog entry for agentic checkout backslash fix
* Add Store API field-validation backslash regression test and clarify test messages
* Fix backslashes stripped when reading customer data from session
diff --git a/plugins/woocommerce/changelog/wooplug-6866-fix-backslash-agentic-checkout b/plugins/woocommerce/changelog/wooplug-6866-fix-backslash-agentic-checkout
new file mode 100644
index 00000000000..ff21677562f
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-6866-fix-backslash-agentic-checkout
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix silent backslash corruption in Agentic Checkout buyer/address fields and Store API checkout field validation. Calling wp_unslash() on JSON request body data that is never magic-quoted was stripping real backslashes typed by the buyer.
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php
index 1821aaa2aef..e6ab8928453 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store-session.php
@@ -132,7 +132,7 @@ class WC_Customer_Data_Store_Session extends WC_Data_Store_WP implements WC_Cust
}
}
} else {
- $customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) );
+ $customer->{"set_{$function_key}"}( $data[ $session_key ] );
}
}
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index 575a73d1dbe..3a07b7605cb 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -277,8 +277,8 @@ class Checkout extends AbstractCartRoute {
continue;
}
- // Clean the field value to trim whitespace.
- $field_value = wc_clean( wp_unslash( $field_values[ $field_key ] ?? '' ) );
+ // Clean the field value to trim whitespace. Request body is JSON-decoded and never magic-quoted, so no wp_unslash().
+ $field_value = wc_clean( $field_values[ $field_key ] ?? '' );
if ( empty( $field_value ) ) {
if ( true === $field['required'] ) {
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php b/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php
index 496db75af86..b51d064fa02 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php
@@ -168,26 +168,26 @@ class AgenticCheckoutUtils {
*/
public static function set_buyer_data( $buyer, $customer ) {
if ( isset( $buyer['first_name'] ) ) {
- $first_name = wc_clean( wp_unslash( $buyer['first_name'] ) );
+ $first_name = wc_clean( $buyer['first_name'] );
$customer->set_billing_first_name( $first_name );
$customer->set_shipping_first_name( $first_name );
}
if ( isset( $buyer['last_name'] ) ) {
- $last_name = wc_clean( wp_unslash( $buyer['last_name'] ) );
+ $last_name = wc_clean( $buyer['last_name'] );
$customer->set_billing_last_name( $last_name );
$customer->set_shipping_last_name( $last_name );
}
if ( isset( $buyer['email'] ) ) {
- $email = sanitize_email( wp_unslash( $buyer['email'] ) );
+ $email = sanitize_email( $buyer['email'] );
if ( is_email( $email ) ) {
$customer->set_billing_email( $email );
}
}
if ( isset( $buyer['phone_number'] ) ) {
- $phone = wc_clean( wp_unslash( $buyer['phone_number'] ) );
+ $phone = wc_clean( $buyer['phone_number'] );
$customer->set_billing_phone( $phone );
}
@@ -203,7 +203,7 @@ class AgenticCheckoutUtils {
public static function set_fulfillment_address( $address, $customer ) {
// Only parse and set name if provided and non-empty.
if ( ! empty( $address['name'] ) ) {
- $name = wc_clean( wp_unslash( $address['name'] ) );
+ $name = wc_clean( $address['name'] );
$name_parts = explode( ' ', $name, 2 );
$first_name = $name_parts[0];
$last_name = isset( $name_parts[1] ) ? $name_parts[1] : '';
@@ -218,12 +218,12 @@ class AgenticCheckoutUtils {
}
// Sanitize all address fields.
- $line_one = wc_clean( wp_unslash( $address['line_one'] ?? '' ) );
- $line_two = wc_clean( wp_unslash( $address['line_two'] ?? '' ) );
- $city = wc_clean( wp_unslash( $address['city'] ?? '' ) );
- $state = wc_clean( wp_unslash( $address['state'] ?? '' ) );
- $postal_code = wc_clean( wp_unslash( $address['postal_code'] ?? '' ) );
- $country = wc_clean( wp_unslash( $address['country'] ?? '' ) );
+ $line_one = wc_clean( $address['line_one'] ?? '' );
+ $line_two = wc_clean( $address['line_two'] ?? '' );
+ $city = wc_clean( $address['city'] ?? '' );
+ $state = wc_clean( $address['state'] ?? '' );
+ $postal_code = wc_clean( $address['postal_code'] ?? '' );
+ $country = wc_clean( $address['country'] ?? '' );
// Set shipping address fields.
$customer->set_shipping_address_1( $line_one );
@@ -279,7 +279,7 @@ class AgenticCheckoutUtils {
public static function set_billing_address( $address, $customer ) {
// Only parse and set name if provided and non-empty.
if ( ! empty( $address['name'] ) ) {
- $name = wc_clean( wp_unslash( $address['name'] ) );
+ $name = wc_clean( $address['name'] );
$name_parts = explode( ' ', $name, 2 );
$first_name = $name_parts[0];
$last_name = isset( $name_parts[1] ) ? $name_parts[1] : '';
@@ -290,12 +290,12 @@ class AgenticCheckoutUtils {
}
// Sanitize all address fields.
- $line_one = wc_clean( wp_unslash( $address['line_one'] ?? '' ) );
- $line_two = wc_clean( wp_unslash( $address['line_two'] ?? '' ) );
- $city = wc_clean( wp_unslash( $address['city'] ?? '' ) );
- $state = wc_clean( wp_unslash( $address['state'] ?? '' ) );
- $postal_code = wc_clean( wp_unslash( $address['postal_code'] ?? '' ) );
- $country = wc_clean( wp_unslash( $address['country'] ?? '' ) );
+ $line_one = wc_clean( $address['line_one'] ?? '' );
+ $line_two = wc_clean( $address['line_two'] ?? '' );
+ $city = wc_clean( $address['city'] ?? '' );
+ $state = wc_clean( $address['state'] ?? '' );
+ $postal_code = wc_clean( $address['postal_code'] ?? '' );
+ $country = wc_clean( $address['country'] ?? '' );
// Set billing address fields.
$customer->set_billing_address_1( $line_one );
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php
index 31f94e7f0cd..7f0b0eec713 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php
@@ -199,4 +199,34 @@ class WC_Customer_Data_Store_Session_Test extends WC_Unit_Test_Case {
$this->assertSame( '', $customer->get_shipping_address_2() );
}
+
+ /**
+ * Ensure backslashes in customer fields survive a session save/read round-trip.
+ *
+ * Reproduces the Agentic Checkout flow where one request stores the address in the session and a
+ * follow-up request reads it back: the session store must not wp_unslash() the data on read, or
+ * real backslashes the buyer typed get stripped (session data is stored raw, never magic-quoted).
+ *
+ * @see https://github.com/woocommerce/woocommerce/pull/65900
+ */
+ public function test_backslashes_survive_session_round_trip() {
+ WC()->session->init();
+
+ $customer = new WC_Customer();
+ $customer->set_email( 'backslash@example.com' );
+ $customer->set_shipping_address_1( 'Apt 4\\B' );
+ $customer->set_shipping_city( 'C:\\Users' );
+ $customer->save();
+
+ $data_store = new WC_Customer_Data_Store_Session();
+ $data_store->save_to_session( $customer );
+
+ // Overwrite in memory, then read back from the session as the next request would.
+ $customer->set_shipping_address_1( 'overwritten' );
+ $customer->set_shipping_city( 'overwritten' );
+ $data_store->read( $customer );
+
+ $this->assertSame( 'Apt 4\\B', $customer->get_shipping_address_1(), 'Backslashes in the shipping address should survive the session round-trip.' );
+ $this->assertSame( 'C:\\Users', $customer->get_shipping_city(), 'Backslashes in the shipping city should survive the session round-trip.' );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
index e75464924df..b5cc8ed1dae 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
@@ -1567,6 +1567,71 @@ class AdditionalFields extends MockeryTestCase {
$this->assertFalse( $this->controller->is_field( $id ), \sprintf( '%s is still registered', $id ) );
}
+ /**
+ * Ensures backslashes survive field validation. The request body is JSON-decoded and never
+ * magic-quoted, so Checkout::validate_callback() must not wp_unslash() the value before validating.
+ */
+ public function test_placing_order_preserves_backslashes_in_validation() {
+ $id = 'plugin-namespace/backslash-validate';
+ \woocommerce_register_additional_checkout_field(
+ array(
+ 'id' => $id,
+ 'label' => 'Backslash Validate',
+ 'location' => 'order',
+ 'type' => 'text',
+ 'validate_callback' => function ( $value ) {
+ // Passes only if the real backslash survived; wp_unslash() would have stripped it.
+ return false !== strpos( $value, '\\' );
+ },
+ )
+ );
+ $request = new \WP_REST_Request( 'POST', '/wc/store/v1/checkout' );
+ $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
+ $request->set_body_params(
+ array(
+ 'billing_address' => (object) array(
+ 'first_name' => 'test',
+ 'last_name' => 'test',
+ 'company' => '',
+ 'address_1' => 'test',
+ 'address_2' => '',
+ 'city' => 'test',
+ 'state' => '',
+ 'postcode' => 'cb241ab',
+ 'country' => 'GB',
+ 'phone' => '5555551234',
+ 'email' => 'testaccount@test.com',
+ 'plugin-namespace/gov-id' => 'my-gov-id',
+ ),
+ 'shipping_address' => (object) array(
+ 'first_name' => 'test',
+ 'last_name' => 'test',
+ 'company' => '',
+ 'address_1' => 'test',
+ 'address_2' => '',
+ 'city' => 'test',
+ 'state' => '',
+ 'postcode' => 'cb241ab',
+ 'country' => 'GB',
+ 'phone' => '5555551234',
+ 'plugin-namespace/gov-id' => 'my-gov-id',
+ ),
+ 'payment_method' => WC_Gateway_BACS::ID,
+ 'additional_fields' => array(
+ 'plugin-namespace/job-function' => 'engineering',
+ $id => 'C:\\Users',
+ ),
+ )
+ );
+
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertEquals( 200, $response->get_status(), print_r( $data, true ) );
+
+ \__internal_woocommerce_blocks_deregister_checkout_field( $id );
+ }
+
/**
* Ensures that the provided validate callback works and prevents an order.
*/
diff --git a/plugins/woocommerce/tests/php/src/StoreApi/Utilities/AgenticCheckoutUtilsTest.php b/plugins/woocommerce/tests/php/src/StoreApi/Utilities/AgenticCheckoutUtilsTest.php
index e71f60549c9..98ffb196733 100644
--- a/plugins/woocommerce/tests/php/src/StoreApi/Utilities/AgenticCheckoutUtilsTest.php
+++ b/plugins/woocommerce/tests/php/src/StoreApi/Utilities/AgenticCheckoutUtilsTest.php
@@ -195,4 +195,62 @@ class AgenticCheckoutUtilsTest extends \WC_Unit_Test_Case {
$result->get_error_message()
);
}
+
+ /**
+ * @testdox Should preserve backslashes in buyer data, which arrives JSON-decoded and is never magic-quoted.
+ */
+ public function test_set_buyer_data_preserves_backslashes() {
+ $customer = WC()->customer;
+
+ AgenticCheckoutUtils::set_buyer_data(
+ array(
+ 'first_name' => 'C:\\Users',
+ 'last_name' => 'O\\Brien',
+ ),
+ $customer
+ );
+
+ $this->assertSame( 'C:\\Users', $customer->get_billing_first_name(), 'Backslashes in first name should be preserved.' );
+ $this->assertSame( 'O\\Brien', $customer->get_billing_last_name(), 'Backslashes in last name should be preserved.' );
+ }
+
+ /**
+ * @testdox Should preserve backslashes in fulfillment address fields.
+ */
+ public function test_set_fulfillment_address_preserves_backslashes() {
+ $customer = WC()->customer;
+
+ AgenticCheckoutUtils::set_fulfillment_address(
+ array(
+ 'name' => 'O\\Brien Family',
+ 'line_one' => 'Apt 4\\B',
+ 'city' => 'Düsseldorf\\Nord',
+ 'country' => 'US',
+ 'postal_code' => '90210',
+ ),
+ $customer
+ );
+
+ $this->assertSame( 'O\\Brien', $customer->get_shipping_first_name(), 'Backslashes in the parsed first name should be preserved.' );
+ $this->assertSame( 'Apt 4\\B', $customer->get_shipping_address_1(), 'Backslashes in address line should be preserved.' );
+ $this->assertSame( 'Düsseldorf\\Nord', $customer->get_shipping_city(), 'Backslashes in city should be preserved.' );
+ }
+
+ /**
+ * @testdox Should preserve backslashes in billing address fields.
+ */
+ public function test_set_billing_address_preserves_backslashes() {
+ $customer = WC()->customer;
+
+ AgenticCheckoutUtils::set_billing_address(
+ array(
+ 'name' => 'O\\Brien Family',
+ 'line_one' => 'Apt 4\\B',
+ ),
+ $customer
+ );
+
+ $this->assertSame( 'O\\Brien', $customer->get_billing_first_name(), 'Backslashes in the parsed first name should be preserved.' );
+ $this->assertSame( 'Apt 4\\B', $customer->get_billing_address_1(), 'Backslashes in address line should be preserved.' );
+ }
}