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' ), '' ),
+ );
+ }
+}