Commit 83208e18335 for woocommerce

commit 83208e18335a99a8d02fe46b595bd61bae4c0e04
Author: Darren Ethier <darren@roughsmootheng.in>
Date:   Fri Aug 21 14:25:16 2026 -0400

    Properly sanitize product category display types (#67893)

diff --git a/plugins/woocommerce/changelog/fix-product-category-display-type-normalization b/plugins/woocommerce/changelog/fix-product-category-display-type-normalization
new file mode 100644
index 00000000000..da0c76d059f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-category-display-type-normalization
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Properly sanitize product category display settings before storage.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php b/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
index 4715a6998f9..bc55c813400 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
@@ -314,8 +314,10 @@ class WC_Admin_Taxonomies {
 	 * @param string $taxonomy Taxonomy slug.
 	 */
 	public function save_category_fields( $term_id, $tt_id = '', $taxonomy = '' ) {
-		if ( isset( $_POST['display_type'] ) && 'product_cat' === $taxonomy ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Core term-edit flow supplies authorization checks; IDs are cast.
-			update_term_meta( $term_id, 'display_type', esc_attr( $_POST['display_type'] ) ); // WPCS: CSRF ok, sanitization ok, input var ok.
+		if ( isset( $_POST['display_type'] ) && 'product_cat' === $taxonomy ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Core term-edit flow verifies the nonce before firing this hook.
+			$display_type = is_string( $_POST['display_type'] ) ? sanitize_key( $_POST['display_type'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Core term-edit flow verifies the nonce before firing this hook.
+
+			update_term_meta( $term_id, 'display_type', $display_type );
 		}
 		if ( isset( $_POST['product_cat_thumbnail_id'] ) && 'product_cat' === $taxonomy ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Core term-edit flow supplies authorization checks; IDs are cast.
 			update_term_meta( $term_id, 'thumbnail_id', absint( $_POST['product_cat_thumbnail_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Core term-edit flow supplies authorization checks; IDs are cast.
@@ -417,7 +419,8 @@ class WC_Admin_Taxonomies {
 		if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) {
 			$make_default_id = absint( $_GET['tag_ID'] );

-			if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok.
+			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- wp_verify_nonce() validates the raw nonce value.
+			if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) {
 				update_option( 'default_product_cat', $make_default_id );
 			}
 		}
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-taxonomies-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-taxonomies-test.php
new file mode 100644
index 00000000000..2bf8e8ea53e
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-taxonomies-test.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Tests for WC_Admin_Taxonomies.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+
+declare( strict_types = 1 );
+
+require_once WC_ABSPATH . 'includes/admin/class-wc-admin-taxonomies.php';
+
+/**
+ * WC_Admin_Taxonomies tests.
+ */
+class WC_Admin_Taxonomies_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var WC_Admin_Taxonomies
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->sut = WC_Admin_Taxonomies::get_instance();
+		$_POST     = array();
+	}
+
+	/**
+	 * Restore request and user state.
+	 */
+	public function tearDown(): void {
+		$_POST = array();
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Should normalize product category display types before storage.
+	 * @dataProvider display_type_provider
+	 *
+	 * @param mixed  $request_value Request display type.
+	 * @param string $expected      Expected stored display type.
+	 */
+	public function test_save_category_fields_normalizes_display_type( $request_value, string $expected ): void {
+		$term_id = $this->factory()->term->create(
+			array(
+				'taxonomy' => 'product_cat',
+				'name'     => 'Display type test',
+			)
+		);
+
+		$_POST['display_type'] = $request_value;
+
+		$this->sut->save_category_fields( $term_id, '', 'product_cat' );
+
+		$this->assertSame( $expected, get_term_meta( $term_id, 'display_type', true ), 'Display type should be normalized before storage.' );
+	}
+
+	/**
+	 * Data provider for display type normalization.
+	 *
+	 * @return array<string, array{mixed, string}>
+	 */
+	public function display_type_provider(): array {
+		return array(
+			'core value'       => array( 'products', 'products' ),
+			'slashed value'    => array( 'sub\\categories', 'subcategories' ),
+			'extension value'  => array( 'custom-layout', 'custom-layout' ),
+			'non-string value' => array( array( 'both' ), '' ),
+		);
+	}
+}