Commit 9d611f856df for woocommerce

commit 9d611f856df323232d43494b23cf40eb1898e590
Author: Mike Jolley <mike.jolley@me.com>
Date:   Mon Nov 9 15:08:05 2026 +0000

    Keep backslashes in product and coupon fields saved outside the admin (#70240)

    * Fix backslashes stripped from product and coupon post fields

    The product, variation and coupon data stores passed unslashed values to wp_insert_post() and wp_update_post(), which unslash their input, so backslashes were removed from names, descriptions and coupon codes. Slash the post data before saving, and drop the workarounds that relied on the old strip: the REST controllers and the product abilities now use wp_kses_post() instead of wp_filter_post_kses(), and the CSV importer no longer doubles the backslash in literal \n text.

    * Add changelog entry for backslashes in product and coupon fields

    * Fix backslashes stripped from coupon codes applied through the Store API

    REST request params are already unslashed, so wp_unslash() removed backslashes from the code and coupons such as save\10 no longer matched once their codes are stored with the backslash. cart/remove-coupon already reads the code as-is.

    * Update changelog entry for backslashes in applied coupon codes

    * Add docblocks for the woocommerce_new_*_data filters

    Docblocks type the filtered post data as an array, so PHPStan no longer needs an ignore on the wp_slash() call, and note that the values are unslashed.

    * Keep the old behavior for post data that is already slashed

    Extensions that pass already-slashed values to the setters, such as the output of wp_filter_post_kses(), relied on the data stores not slashing post data. Leave values that are already slashed as they are, so those extensions keep working while they migrate. This check is temporary.

    * Align assignments in the product REST backslash tests

    * Remove the resolved wp_update_post baseline entry for the product data store

diff --git a/plugins/woocommerce/changelog/fix-69755-backslashes-in-product-coupon-fields b/plugins/woocommerce/changelog/fix-69755-backslashes-in-product-coupon-fields
new file mode 100644
index 00000000000..aa04061f312
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-69755-backslashes-in-product-coupon-fields
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep backslashes in product names, descriptions and coupon codes saved through the REST API, CSV import or code, and in coupons applied at checkout.
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
index c4b7f552bfe..851e114ae93 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
@@ -6,6 +6,7 @@
  */

 use Automattic\WooCommerce\Internal\Caches\CouponCodeLookupInvalidator;
+use Automattic\WooCommerce\Internal\DataStores\PostDataSlashing;

 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
@@ -82,23 +83,30 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
 			$coupon->set_date_created( time() );
 		}

-		$coupon_id = wp_insert_post(
-			apply_filters(
-				'woocommerce_new_coupon_data',
-				array(
-					'post_type'     => 'shop_coupon',
-					'post_status'   => $coupon->get_status( 'edit' ) ? $coupon->get_status( 'edit' ) : 'publish',
-					'post_author'   => get_current_user_id(),
-					'post_title'    => $coupon->get_code( 'edit' ),
-					'post_content'  => '',
-					'post_excerpt'  => $coupon->get_description( 'edit' ),
-					'post_date'     => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
-					'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
-				)
-			),
-			true
+		/**
+		 * Filters the post data used to create a coupon.
+		 *
+		 * Values are unslashed. They are slashed before being passed to wp_insert_post().
+		 *
+		 * @since 3.0.0
+		 * @param array $data Post data for wp_insert_post().
+		 */
+		$post_data = apply_filters(
+			'woocommerce_new_coupon_data',
+			array(
+				'post_type'     => 'shop_coupon',
+				'post_status'   => $coupon->get_status( 'edit' ) ? $coupon->get_status( 'edit' ) : 'publish',
+				'post_author'   => get_current_user_id(),
+				'post_title'    => $coupon->get_code( 'edit' ),
+				'post_content'  => '',
+				'post_excerpt'  => $coupon->get_description( 'edit' ),
+				'post_date'     => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
+				'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
+			)
 		);

+		$coupon_id = wp_insert_post( PostDataSlashing::slash( $post_data ), true );
+
 		if ( $coupon_id ) {
 			$coupon->set_id( $coupon_id );
 			$this->update_post_meta( $coupon );
@@ -215,7 +223,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
 				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) );
 				clean_post_cache( $coupon->get_id() );
 			} else {
-				wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) );
+				wp_update_post( PostDataSlashing::slash( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) ) );
 			}
 			$coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
 		}
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index 509e0e34ceb..b6f22875639 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\CatalogVisibility;
 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
+use Automattic\WooCommerce\Internal\DataStores\PostDataSlashing;
 use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
 use Automattic\WooCommerce\Utilities\NumberUtil;
 use Automattic\WooCommerce\Enums\ProductStockStatus;
@@ -215,29 +216,36 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 			$product->set_date_created( time() );
 		}

-		$id = wp_insert_post(
-			apply_filters(
-				'woocommerce_new_product_data',
-				array(
-					'post_type'      => 'product',
-					'post_status'    => $product->get_status() ? $product->get_status() : ProductStatus::PUBLISH,
-					'post_author'    => get_current_user_id(),
-					'post_title'     => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
-					'post_content'   => $product->get_description(),
-					'post_excerpt'   => $product->get_short_description(),
-					'post_parent'    => $product->get_parent_id(),
-					'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
-					'ping_status'    => 'closed',
-					'menu_order'     => $product->get_menu_order(),
-					'post_password'  => $product->get_post_password( 'edit' ),
-					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
-					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
-					'post_name'      => $product->get_slug( 'edit' ),
-				)
-			),
-			true
+		/**
+		 * Filters the post data used to create a product.
+		 *
+		 * Values are unslashed. They are slashed before being passed to wp_insert_post().
+		 *
+		 * @since 3.0.0
+		 * @param array $data Post data for wp_insert_post().
+		 */
+		$post_data = apply_filters(
+			'woocommerce_new_product_data',
+			array(
+				'post_type'      => 'product',
+				'post_status'    => $product->get_status() ? $product->get_status() : ProductStatus::PUBLISH,
+				'post_author'    => get_current_user_id(),
+				'post_title'     => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
+				'post_content'   => $product->get_description(),
+				'post_excerpt'   => $product->get_short_description(),
+				'post_parent'    => $product->get_parent_id(),
+				'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
+				'ping_status'    => 'closed',
+				'menu_order'     => $product->get_menu_order(),
+				'post_password'  => $product->get_post_password( 'edit' ),
+				'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
+				'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
+				'post_name'      => $product->get_slug( 'edit' ),
+			)
 		);

+		$id = wp_insert_post( PostDataSlashing::slash( $post_data ), true );
+
 		if ( $id && ! is_wp_error( $id ) ) {
 			$product->set_id( $id );
 			$sku = $product->get_sku();
@@ -373,7 +381,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
 				clean_post_cache( $product->get_id() );
 			} else {
-				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
+				wp_update_post( PostDataSlashing::slash( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ) );
 			}
 			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.

diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
index 0526ba33f66..570437d43cd 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
@@ -8,6 +8,7 @@
 use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Enums\CatalogVisibility;
+use Automattic\WooCommerce\Internal\DataStores\PostDataSlashing;
 use Automattic\WooCommerce\Enums\ProductStockStatus;

 if ( ! defined( 'ABSPATH' ) ) {
@@ -132,28 +133,35 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 			$product->set_parent_id( 0 );
 		}

-		$id = wp_insert_post(
-			apply_filters(
-				'woocommerce_new_product_variation_data',
-				array(
-					'post_type'      => 'product_variation',
-					'post_status'    => $product->get_status() ? $product->get_status() : ProductStatus::PUBLISH,
-					'post_author'    => get_current_user_id(),
-					'post_title'     => $product->get_name( 'edit' ),
-					'post_excerpt'   => $product->get_attribute_summary( 'edit' ),
-					'post_content'   => '',
-					'post_parent'    => $product->get_parent_id(),
-					'comment_status' => 'closed',
-					'ping_status'    => 'closed',
-					'menu_order'     => $product->get_menu_order(),
-					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
-					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
-					'post_name'      => $product->get_slug( 'edit' ),
-				)
-			),
-			true
+		/**
+		 * Filters the post data used to create a product variation.
+		 *
+		 * Values are unslashed. They are slashed before being passed to wp_insert_post().
+		 *
+		 * @since 3.0.0
+		 * @param array $data Post data for wp_insert_post().
+		 */
+		$post_data = apply_filters(
+			'woocommerce_new_product_variation_data',
+			array(
+				'post_type'      => 'product_variation',
+				'post_status'    => $product->get_status() ? $product->get_status() : ProductStatus::PUBLISH,
+				'post_author'    => get_current_user_id(),
+				'post_title'     => $product->get_name( 'edit' ),
+				'post_excerpt'   => $product->get_attribute_summary( 'edit' ),
+				'post_content'   => '',
+				'post_parent'    => $product->get_parent_id(),
+				'comment_status' => 'closed',
+				'ping_status'    => 'closed',
+				'menu_order'     => $product->get_menu_order(),
+				'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
+				'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
+				'post_name'      => $product->get_slug( 'edit' ),
+			)
 		);

+		$id = wp_insert_post( PostDataSlashing::slash( $post_data ), true );
+
 		if ( $id && ! is_wp_error( $id ) ) {
 			$product->set_id( $id );

@@ -246,7 +254,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
 				clean_post_cache( $product->get_id() );
 			} else {
-				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
+				wp_update_post( PostDataSlashing::slash( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ) );
 			}
 			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.

diff --git a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
index dcdee146341..22528500780 100644
--- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
+++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
@@ -1007,7 +1007,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 			$parts[ $key ] = str_replace( '\n', "\n", $part );
 		}

-		return implode( '\\\n', $parts );
+		return implode( '\n', $parts );
 	}

 	/**
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php
index 4295758ea7b..510d525794f 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php
@@ -275,7 +275,7 @@ class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller {
 						$coupon->set_code( $coupon_code );
 						break;
 					case 'description' :
-						$coupon->set_description( wp_filter_post_kses( $value ) );
+						$coupon->set_description( wp_kses_post( $value ) );
 						break;
 					case 'expiry_date' :
 						$coupon->set_date_expires( $value );
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php
index 36083d30c21..6c575a0f245 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php
@@ -697,17 +697,17 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller {

 		// Post title.
 		if ( isset( $request['name'] ) ) {
-			$product->set_name( wp_filter_post_kses( $request['name'] ) );
+			$product->set_name( wp_kses_post( $request['name'] ) );
 		}

 		// Post content.
 		if ( isset( $request['description'] ) ) {
-			$product->set_description( wp_filter_post_kses( $request['description'] ) );
+			$product->set_description( wp_kses_post( $request['description'] ) );
 		}

 		// Post excerpt.
 		if ( isset( $request['short_description'] ) ) {
-			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
+			$product->set_short_description( wp_kses_post( $request['short_description'] ) );
 		}

 		// Post status.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php
index 8a77304accc..77cc557acf3 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php
@@ -339,7 +339,7 @@ class WC_REST_Coupons_V2_Controller extends WC_REST_CRUD_Controller {
 						MetaDataUtil::update( $value, $coupon );
 						break;
 					case 'description':
-						$coupon->set_description( wp_filter_post_kses( $value ) );
+						$coupon->set_description( wp_kses_post( $value ) );
 						break;
 					default:
 						if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
index c7fb0151dc3..4353e8c859f 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
@@ -1108,17 +1108,17 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {

 		// Post title.
 		if ( isset( $request['name'] ) ) {
-			$product->set_name( wp_filter_post_kses( $request['name'] ) );
+			$product->set_name( wp_kses_post( $request['name'] ) );
 		}

 		// Post content.
 		if ( isset( $request['description'] ) ) {
-			$product->set_description( wp_filter_post_kses( $request['description'] ) );
+			$product->set_description( wp_kses_post( $request['description'] ) );
 		}

 		// Post excerpt.
 		if ( isset( $request['short_description'] ) ) {
-			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
+			$product->set_short_description( wp_kses_post( $request['short_description'] ) );
 		}

 		// Post status.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index 86b1d783a62..6ff6142eb1f 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -818,17 +818,17 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {

 		// Post title.
 		if ( isset( $request['name'] ) ) {
-			$product->set_name( wp_filter_post_kses( $request['name'] ) );
+			$product->set_name( wp_kses_post( $request['name'] ) );
 		}

 		// Post content.
 		if ( isset( $request['description'] ) ) {
-			$product->set_description( wp_filter_post_kses( $request['description'] ) );
+			$product->set_description( wp_kses_post( $request['description'] ) );
 		}

 		// Post excerpt.
 		if ( isset( $request['short_description'] ) ) {
-			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
+			$product->set_short_description( wp_kses_post( $request['short_description'] ) );
 		}

 		// Post status.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index e6f3ca6c80e..8538d4aba71 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -17382,12 +17382,6 @@ parameters:
 			count: 4
 			path: includes/data-stores/class-wc-product-data-store-cpt.php

-		-
-			message: '#^Parameter \#1 \$postarr of function wp_update_post expects array\{ID\?\: int, post_author\?\: int, post_date\?\: string, post_date_gmt\?\: string, post_content\?\: string, post_content_filtered\?\: string, post_title\?\: string, post_excerpt\?\: string, \.\.\.\}, array\{ID\: int, post_content\: string, post_excerpt\: string, post_title\: string, post_parent\: int, comment_status\: ''closed''\|''open'', post_status\: non\-falsy\-string, menu_order\: int, \.\.\.\} given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/data-stores/class-wc-product-data-store-cpt.php
-
 		-
 			message: '#^Parameter \#1 \$posts of function update_post_caches expects array\<WP_Post\>, array\<int\|WP_Post\> given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAbilityTrait.php
index 17f684e7a90..41d1003ad61 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAbilityTrait.php
@@ -1445,8 +1445,8 @@ trait ProductAbilityTrait {
 	 * @return mixed|\WP_Error
 	 */
 	private static function prepare_product_field_value( string $field, $value ) {
-		if ( in_array( $field, array( 'name', 'description', 'short_description' ), true ) ) {
-			return wp_filter_post_kses( $value );
+		if ( in_array( $field, array( 'name', 'description', 'short_description' ), true ) && is_string( $value ) ) {
+			return wp_kses_post( $value );
 		}

 		if ( in_array( $field, array( 'status', 'stock_status', 'external_url' ), true ) ) {
diff --git a/plugins/woocommerce/src/Internal/DataStores/PostDataSlashing.php b/plugins/woocommerce/src/Internal/DataStores/PostDataSlashing.php
new file mode 100644
index 00000000000..4367f66d690
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/DataStores/PostDataSlashing.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * PostDataSlashing class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores;
+
+/**
+ * Slashes post data for wp_insert_post() and wp_update_post(), which unslash it.
+ *
+ * @since 11.4.0
+ */
+class PostDataSlashing {
+
+	/**
+	 * Slash the string values in post data, so backslashes survive wp_insert_post() and wp_update_post().
+	 *
+	 * For backwards compatibility, values that are already slashed (such as the output of wp_filter_post_kses()) are
+	 * passed as they are, so they're still unslashed once as before. This check is temporary and will be removed.
+	 *
+	 * @param mixed $data Post data.
+	 * @return mixed
+	 */
+	public static function slash( $data ) {
+		if ( ! is_array( $data ) ) {
+			return $data;
+		}
+
+		foreach ( $data as $key => $value ) {
+			if ( is_string( $value ) && wp_slash( wp_unslash( $value ) ) !== $value ) {
+				$data[ $key ] = wp_slash( $value );
+			}
+		}
+
+		return $data;
+	}
+}
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 bab21f2c402..a2fecb075e7 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -1029,17 +1029,17 @@ class Controller extends WC_REST_Products_V2_Controller {

 		// Post title.
 		if ( isset( $request['name'] ) ) {
-			$product->set_name( wp_filter_post_kses( $request['name'] ) );
+			$product->set_name( wp_kses_post( $request['name'] ) );
 		}

 		// Post content.
 		if ( isset( $request['description'] ) ) {
-			$product->set_description( wp_filter_post_kses( $request['description'] ) );
+			$product->set_description( wp_kses_post( $request['description'] ) );
 		}

 		// Post excerpt.
 		if ( isset( $request['short_description'] ) ) {
-			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
+			$product->set_short_description( wp_kses_post( $request['short_description'] ) );
 		}

 		// Post status.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php
index 56fffb558b2..b334d27f7f3 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php
@@ -67,7 +67,7 @@ class CartApplyCoupon extends AbstractCartRoute {
 			throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', esc_html__( 'Coupons are disabled.', 'woocommerce' ), 404 );
 		}

-		$coupon_code = wc_format_coupon_code( wp_unslash( $request['code'] ) );
+		$coupon_code = wc_format_coupon_code( $request['code'] );

 		try {
 			$this->cart_controller->apply_coupon( $coupon_code );
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-coupon-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-coupon-data-store-cpt-test.php
index e3eefc5de7f..80faa58a46d 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-coupon-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-coupon-data-store-cpt-test.php
@@ -388,4 +388,38 @@ class WC_Coupon_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$this->assertNull( $reloaded_coupon->get_date_created( 'edit' ) );
 		$this->assertSame( 'Updated description', $reloaded_coupon->get_description() );
 	}
+
+	/**
+	 * @testdox Creating and updating a coupon keeps backslashes and quotes in its code and description.
+	 */
+	public function test_create_and_update_keep_backslashes_in_post_fields(): void {
+		$coupon = new WC_Coupon();
+		$coupon->set_code( 'save\\10' );
+		$coupon->set_description( 'Path C:\\temp, it\'s "quoted"' );
+		$coupon->save();
+
+		$post = get_post( $coupon->get_id() );
+		$this->assertSame( 'save\\10', $post->post_title, 'The created code should keep its backslash.' );
+		$this->assertSame( 'Path C:\\temp, it\'s "quoted"', $post->post_excerpt, 'The created description should keep its backslash and quotes.' );
+
+		$coupon->set_code( 'save\\20' );
+		$coupon->set_description( 'Path D:\\temp, it\'s "quoted"' );
+		$coupon->save();
+
+		$post = get_post( $coupon->get_id() );
+		$this->assertSame( 'save\\20', $post->post_title, 'The updated code should keep its backslash.' );
+		$this->assertSame( 'Path D:\\temp, it\'s "quoted"', $post->post_excerpt, 'The updated description should keep its backslash and quotes.' );
+	}
+
+	/**
+	 * @testdox Saving a coupon keeps the existing behavior for a description that is already slashed.
+	 */
+	public function test_save_unslashes_already_slashed_description(): void {
+		$coupon = new WC_Coupon();
+		$coupon->set_code( 'slashed-description' );
+		$coupon->set_description( wp_filter_post_kses( 'It\'s 12" off' ) );
+		$coupon->save();
+
+		$this->assertSame( 'It\'s 12" off', get_post( $coupon->get_id() )->post_excerpt );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
index 49385908f8d..fe1928e8f0d 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
@@ -893,4 +893,88 @@ class WC_Product_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$this->assertFalse( ObjectInstantiationProbe::$was_unserialized, 'Stored product meta must not instantiate objects' );
 		$this->assertSame( array( 'size' => 'small' ), $product->get_default_attributes(), 'Scalar default attributes should still load' );
 	}
+
+	/**
+	 * @testdox Creating and updating a product keeps backslashes and quotes in its name and descriptions.
+	 */
+	public function test_create_and_update_keep_backslashes_in_post_fields(): void {
+		$product = new WC_Product_Simple();
+		$product->set_name( 'Cable 1\\2" it\'s' );
+		$product->set_description( 'Path C:\\temp' );
+		$product->set_short_description( 'Short C:\\temp' );
+		$product->save();
+
+		$post = get_post( $product->get_id() );
+		$this->assertSame( 'Cable 1\\2" it\'s', $post->post_title, 'The created name should keep its backslash and quotes.' );
+		$this->assertSame( 'Path C:\\temp', $post->post_content, 'The created description should keep its backslash.' );
+		$this->assertSame( 'Short C:\\temp', $post->post_excerpt, 'The created short description should keep its backslash.' );
+
+		$product->set_name( 'Cable 3\\4" it\'s' );
+		$product->set_description( 'Path D:\\temp' );
+		$product->set_short_description( 'Short D:\\temp' );
+		$product->save();
+
+		$post = get_post( $product->get_id() );
+		$this->assertSame( 'Cable 3\\4" it\'s', $post->post_title, 'The updated name should keep its backslash and quotes.' );
+		$this->assertSame( 'Path D:\\temp', $post->post_content, 'The updated description should keep its backslash.' );
+		$this->assertSame( 'Short D:\\temp', $post->post_excerpt, 'The updated short description should keep its backslash.' );
+	}
+
+	/**
+	 * Values in already-slashed form, as extensions pass them after wp_filter_post_kses() or from $_POST.
+	 *
+	 * @return array<string, array{0: string, 1: string}>
+	 */
+	public function provider_already_slashed_post_fields(): array {
+		return array(
+			'output of wp_filter_post_kses()' => array( 'Men\\\'s 12\\" shirt', 'Men\'s 12" shirt' ),
+			'slashed twice'                   => array( 'Men\\\\\\\'s shirt', 'Men\\\'s shirt' ),
+		);
+	}
+
+	/**
+	 * @testdox Saving a product keeps the existing behavior for values that are already slashed.
+	 * @dataProvider provider_already_slashed_post_fields
+	 *
+	 * @param string $value    Value passed to the setters.
+	 * @param string $expected Stored value.
+	 */
+	public function test_save_unslashes_already_slashed_post_fields( string $value, string $expected ): void {
+		$product = new WC_Product_Simple();
+		$product->set_name( $value );
+		$product->set_description( $value );
+		$product->save();
+
+		$post = get_post( $product->get_id() );
+		$this->assertSame( $expected, $post->post_title, 'A created name in slashed form should be unslashed.' );
+		$this->assertSame( $expected, $post->post_content, 'A created description in slashed form should be unslashed.' );
+
+		$product->set_name( 'Renamed' );
+		$product->save();
+		$product->set_name( $value );
+		$product->set_short_description( $value );
+		$product->save();
+
+		$post = get_post( $product->get_id() );
+		$this->assertSame( $expected, $post->post_title, 'An updated name in slashed form should be unslashed.' );
+		$this->assertSame( $expected, $post->post_excerpt, 'An updated short description in slashed form should be unslashed.' );
+	}
+
+	/**
+	 * @testdox A woocommerce_new_product_data callback that slashes its values keeps working.
+	 */
+	public function test_create_unslashes_values_slashed_by_new_product_data_filter(): void {
+		add_filter(
+			'woocommerce_new_product_data',
+			function ( $data ) {
+				$data['post_title'] = wp_slash( 'Men\'s 1\\2 inch' );
+				return $data;
+			}
+		);
+
+		$product = new WC_Product_Simple();
+		$product->save();
+
+		$this->assertSame( 'Men\'s 1\\2 inch', get_post( $product->get_id() )->post_title );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
index 5df7cb2cdbd..f8695a2b33a 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
@@ -326,6 +326,36 @@ class WC_Product_Variation_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$this->assertEmpty( get_the_terms( $variation_id, 'product_type' ) );
 	}

+	/**
+	 * @testdox Creating and updating a variation keeps backslashes from the parent name in its title.
+	 */
+	public function test_create_and_update_keep_backslashes_in_variation_title(): void {
+		$parent = WC_Helper_Product::create_variation_product();
+		$parent->set_name( 'Cable 1\\2 inch' );
+		$parent->save();
+
+		// Record the title WordPress stores, before read() repairs a mismatched title.
+		$stored_titles = array();
+		add_filter(
+			'wp_insert_post_data',
+			function ( $data ) use ( &$stored_titles ) {
+				if ( 'product_variation' === $data['post_type'] ) {
+					$stored_titles[] = wp_unslash( $data['post_title'] );
+				}
+				return $data;
+			},
+			PHP_INT_MAX
+		);
+
+		$variation = new WC_Product_Variation();
+		$variation->set_parent_id( $parent->get_id() );
+		$variation->save();
+		$variation->set_menu_order( 5 );
+		$variation->save();
+
+		$this->assertSame( array( 'Cable 1\\2 inch', 'Cable 1\\2 inch' ), $stored_titles, 'Creating and updating the variation should both store the backslash from the parent name.' );
+	}
+
 	/**
 	 * Create a variable product and return one of its variations.
 	 *
diff --git a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
index a9f504170a9..21de54310d7 100644
--- a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
+++ b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
@@ -3271,4 +3271,29 @@ class WC_Product_CSV_Importer_Test extends \WC_Unit_Test_Case {
 		$this->assertSame( 'woocommerce_product_csv_importer_invalid_id', $result->get_error_code() );
 		$this->assertSame( 'page', get_post_type( $page_id ), 'The page should keep its post type' );
 	}
+
+	/**
+	 * @testdox Importing keeps backslashes in names and descriptions, and turns only escaped newlines into line breaks.
+	 */
+	public function test_import_keeps_backslashes_in_name_and_descriptions(): void {
+		$importer = new WC_Product_CSV_Importer(
+			__DIR__ . '/import-description-backslashes-69755-data.csv',
+			array(
+				'parse'   => true,
+				'mapping' => array(
+					'Type'              => 'type',
+					'Name'              => 'name',
+					'Description'       => 'description',
+					'Short description' => 'short_description',
+				),
+			)
+		);
+		$data     = $importer->import();
+
+		$this->assertCount( 1, $data['imported'], 'The product should be imported.' );
+		$post = get_post( $data['imported'][0] );
+		$this->assertSame( 'Cable 1\\2 inch', $post->post_title, 'The name should keep its backslash.' );
+		$this->assertSame( 'Path C:\\temp\\nliteral', $post->post_content, 'The description should keep its backslash and the literal \\n text.' );
+		$this->assertSame( "Line one\nLine two", $post->post_excerpt, 'An escaped newline should become a line break.' );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/importer/import-description-backslashes-69755-data.csv b/plugins/woocommerce/tests/php/includes/importer/import-description-backslashes-69755-data.csv
new file mode 100644
index 00000000000..20ebdc2c0dd
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/importer/import-description-backslashes-69755-data.csv
@@ -0,0 +1,2 @@
+"Type","Name","Description","Short description"
+"simple","Cable 1\2 inch","Path C:\temp\\nliteral","Line one\nLine two"
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller-tests.php
index b8d1b89218b..c53708a4e9c 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller-tests.php
@@ -344,4 +344,40 @@ class WC_REST_Coupons_Controller_Tests extends WC_REST_Unit_Test_Case {

 		$this->assert_incomplete_meta_data_handled_correctly( new \WC_Coupon( $coupon->get_id() ) );
 	}
+
+	/**
+	 * @testdox Creating and updating a coupon keeps backslashes and quotes in its code and description.
+	 */
+	public function test_create_and_update_coupon_keep_backslashes_in_post_fields(): void {
+		wp_set_current_user( $this->user );
+
+		$request = new WP_REST_Request( 'POST', '/wc/v3/coupons' );
+		$request->set_body_params(
+			array(
+				'code'        => 'save\\10',
+				'description' => 'Path C:\\temp, it\'s "quoted"',
+			)
+		);
+		$response = $this->server->dispatch( $request );
+
+		$this->assertSame( 201, $response->get_status() );
+		$post = get_post( $response->get_data()['id'] );
+		$this->assertSame( 'save\\10', $response->get_data()['code'], 'The response code should keep its backslash.' );
+		$this->assertSame( 'save\\10', $post->post_title, 'The stored code should keep its backslash.' );
+		$this->assertSame( 'Path C:\\temp, it\'s "quoted"', $post->post_excerpt, 'The stored description should keep its backslash and quotes.' );
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v3/coupons/' . $post->ID );
+		$request->set_body_params(
+			array(
+				'code'        => 'save\\20',
+				'description' => 'Path D:\\temp, it\'s "quoted"',
+			)
+		);
+		$response = $this->server->dispatch( $request );
+
+		$this->assertSame( 200, $response->get_status() );
+		$post = get_post( $post->ID );
+		$this->assertSame( 'save\\20', $post->post_title, 'The updated code should keep its backslash.' );
+		$this->assertSame( 'Path D:\\temp, it\'s "quoted"', $post->post_excerpt, 'The updated description should keep its backslash and quotes.' );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
index 4908474e2e8..146ff34720e 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
@@ -2920,4 +2920,48 @@ class WC_REST_Products_Controller_Tests extends WC_Unit_Test_Case {
 			'The featured image should be listed first and only once.'
 		);
 	}
+
+	/**
+	 * @testdox Creating and updating a product keeps backslashes and quotes in its name and descriptions.
+	 */
+	public function test_create_and_update_product_keep_backslashes_in_post_fields(): void {
+		$fields         = array(
+			'name'              => 'Cable 1\\2" it\'s',
+			'description'       => 'Path C:\\temp',
+			'short_description' => 'Short C:\\temp',
+		);
+		$updated_fields = array(
+			'name'              => 'Cable 3\\4" it\'s',
+			'description'       => 'Path D:\\temp',
+			'short_description' => 'Short D:\\temp',
+		);
+
+		$request = new WP_REST_Request( 'POST', '/wc/v3/products' );
+		$request->set_body_params( $fields );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertSame( 201, $response->get_status() );
+		$this->assert_post_fields_keep_backslashes( $response->get_data(), $fields );
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $response->get_data()['id'] );
+		$request->set_body_params( $updated_fields );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertSame( 200, $response->get_status() );
+		$this->assert_post_fields_keep_backslashes( $response->get_data(), $updated_fields );
+	}
+
+	/**
+	 * Assert a product response and its stored post keep the given name and descriptions.
+	 *
+	 * @param array $data   Product response data.
+	 * @param array $fields Expected name, description and short description.
+	 */
+	private function assert_post_fields_keep_backslashes( array $data, array $fields ): void {
+		$post = get_post( $data['id'] );
+		$this->assertSame( $fields['name'], $data['name'], 'The response name should keep its backslash and quotes.' );
+		$this->assertSame( $fields['name'], $post->post_title, 'The stored name should keep its backslash and quotes.' );
+		$this->assertSame( $fields['description'], $post->post_content, 'The stored description should keep its backslash.' );
+		$this->assertSame( $fields['short_description'], $post->post_excerpt, 'The stored short description should keep its backslash.' );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php
index fec8c48448a..96a2d0feacc 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php
@@ -277,6 +277,35 @@ class CartApplyCoupon extends ControllerTestCase {
 		update_option( 'woocommerce_enable_coupons', 'yes' );
 	}

+	/**
+	 * @testdox Applying a coupon whose code contains a backslash keeps the backslash.
+	 */
+	public function test_apply_coupon_with_backslash_in_code(): void {
+		wc()->cart->remove_coupons();
+
+		$fixtures = new FixtureData();
+		$fixtures->get_coupon(
+			array(
+				'code'          => 'save\\10',
+				'discount_type' => 'fixed_cart',
+				'amount'        => 1,
+			)
+		);
+
+		$request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/apply-coupon' );
+		$request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
+		$request->set_body_params(
+			array(
+				'code' => 'save\\10',
+			)
+		);
+
+		$response = rest_get_server()->dispatch( $request );
+
+		$this->assertSame( 200, $response->get_status(), 'The coupon should be applied.' );
+		$this->assertSame( 'save\\10', $response->get_data()['coupons'][0]['code'], 'The applied code should keep its backslash.' );
+	}
+
 	/**
 	 * Test applying invalid coupon.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php b/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php
index 6dece73966a..02d0a420bbd 100644
--- a/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/AbilitiesApi/ExtensibleAbilitiesTest.php
@@ -2371,6 +2371,25 @@ class ExtensibleAbilitiesTest extends \WC_REST_Unit_Test_Case {
 		$this->assertSame( 'Happy birthday', $updated['order']['extensions']['test-ext']['gift_message'] );
 	}

+	/**
+	 * @testdox Should keep backslashes and quotes in the name and descriptions of a created product.
+	 */
+	public function test_product_create_keeps_backslashes_in_post_fields(): void {
+		$created = $this->run_ability(
+			'woocommerce/product-create',
+			array(
+				'name'              => 'Cable 1\\2" it\'s',
+				'description'       => 'Path C:\\temp',
+				'short_description' => 'Short C:\\temp',
+			)
+		);
+		$post    = get_post( $created['product']['id'] );
+
+		$this->assertSame( 'Cable 1\\2" it\'s', $post->post_title, 'The stored name should keep its backslash and quotes.' );
+		$this->assertSame( 'Path C:\\temp', $post->post_content, 'The stored description should keep its backslash.' );
+		$this->assertSame( 'Short C:\\temp', $post->post_excerpt, 'The stored short description should keep its backslash.' );
+	}
+
 	/**
 	 * @testdox Should save nothing when a value is rejected, does not match its schema, or belongs to a read-only field.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/PostDataSlashingTest.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/PostDataSlashingTest.php
new file mode 100644
index 00000000000..1f2f236bf2e
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/PostDataSlashingTest.php
@@ -0,0 +1,54 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\DataStores;
+
+use Automattic\WooCommerce\Internal\DataStores\PostDataSlashing;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the PostDataSlashing class.
+ */
+class PostDataSlashingTest extends WC_Unit_Test_Case {
+
+	/**
+	 * Values and the result of slashing them.
+	 *
+	 * @return array<string, array{0: string, 1: string}>
+	 */
+	public function provider_values(): array {
+		return array(
+			'backslash'                       => array( 'Cable 1\\2 inch', 'Cable 1\\\\2 inch' ),
+			'quotes'                          => array( 'Men\'s 12" shirt', 'Men\\\'s 12\\" shirt' ),
+			'backslash and quote'             => array( 'C:\\temp, it\'s', 'C:\\\\temp, it\\\'s' ),
+			'output of wp_filter_post_kses()' => array( 'Men\\\'s 12\\" shirt', 'Men\\\'s 12\\" shirt' ),
+			'no special characters'           => array( 'Plain name', 'Plain name' ),
+		);
+	}
+
+	/**
+	 * @testdox Should slash string values unless they are already slashed.
+	 * @dataProvider provider_values
+	 *
+	 * @param string $value    Value to slash.
+	 * @param string $expected Expected result.
+	 */
+	public function test_slashes_values_unless_already_slashed( string $value, string $expected ): void {
+		$result = PostDataSlashing::slash( array( 'post_title' => $value ) );
+
+		$this->assertSame( $expected, $result['post_title'] );
+	}
+
+	/**
+	 * @testdox Should leave non-string values and non-array data unchanged.
+	 */
+	public function test_leaves_non_string_values_and_non_array_data_unchanged(): void {
+		$data = array(
+			'menu_order' => 3,
+			'meta_input' => array( '_note' => 'it\'s' ),
+		);
+
+		$this->assertSame( $data, PostDataSlashing::slash( $data ), 'Non-string values should be unchanged.' );
+		$this->assertSame( 'post_title=it\'s', PostDataSlashing::slash( 'post_title=it\'s' ), 'Non-array data should be unchanged.' );
+	}
+}
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 18f209f2831..89b609d693f 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
@@ -2955,4 +2955,48 @@ class ProductsControllerTest extends WC_Unit_Test_Case {
 		$this->assertArrayHasKey( 'cost_of_goods_sold', $data, 'Shop manager should see COGS data' );
 		$this->assertArrayHasKey( 'meta_data', $data, 'Shop manager should see meta data' );
 	}
+
+	/**
+	 * @testdox Creating and updating a product keeps backslashes and quotes in its name and descriptions.
+	 */
+	public function test_create_and_update_product_keep_backslashes_in_post_fields(): void {
+		$fields         = array(
+			'name'              => 'Cable 1\\2" it\'s',
+			'description'       => 'Path C:\\temp',
+			'short_description' => 'Short C:\\temp',
+		);
+		$updated_fields = array(
+			'name'              => 'Cable 3\\4" it\'s',
+			'description'       => 'Path D:\\temp',
+			'short_description' => 'Short D:\\temp',
+		);
+
+		$request = new WP_REST_Request( 'POST', '/wc/v4/products' );
+		$request->set_body_params( $fields );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertSame( 201, $response->get_status() );
+		$this->assert_post_fields_keep_backslashes( $response->get_data(), $fields );
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v4/products/' . $response->get_data()['id'] );
+		$request->set_body_params( $updated_fields );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertSame( 200, $response->get_status() );
+		$this->assert_post_fields_keep_backslashes( $response->get_data(), $updated_fields );
+	}
+
+	/**
+	 * Assert a product response and its stored post keep the given name and descriptions.
+	 *
+	 * @param array $data   Product response data.
+	 * @param array $fields Expected name, description and short description.
+	 */
+	private function assert_post_fields_keep_backslashes( array $data, array $fields ): void {
+		$post = get_post( $data['id'] );
+		$this->assertSame( $fields['name'], $data['name'], 'The response name should keep its backslash and quotes.' );
+		$this->assertSame( $fields['name'], $post->post_title, 'The stored name should keep its backslash and quotes.' );
+		$this->assertSame( $fields['description'], $post->post_content, 'The stored description should keep its backslash.' );
+		$this->assertSame( $fields['short_description'], $post->post_excerpt, 'The stored short description should keep its backslash.' );
+	}
 }