From 2e993b2f967667baf13c97111ac136e8faffb69f Mon Sep 17 00:00:00 2001 From: dwindown Date: Fri, 21 Nov 2025 00:11:29 +0700 Subject: [PATCH] fix(products): Add comprehensive data sanitization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Products module had NO sanitization - fixed to match Orders/Coupons/Customers Issue: ❌ No sanitization in create_product ❌ No sanitization in update_product ❌ Direct assignment of raw user input ❌ Potential XSS and injection vulnerabilities ❌ Inconsistent with other modules Changes Made: 1. Created Sanitization Helpers (Lines 23-65): ✅ sanitize_text() - Text fields (name, SKU) ✅ sanitize_textarea() - Descriptions (allows newlines) ✅ sanitize_number() - Prices, dimensions (removes non-numeric) ✅ sanitize_slug() - URL slugs (uses sanitize_title) 2. Fixed create_product() (Lines 278-317): ✅ Name → sanitize_text() ✅ Slug → sanitize_slug() ✅ Status → sanitize_key() ✅ Description → sanitize_textarea() ✅ Short description → sanitize_textarea() ✅ SKU → sanitize_text() ✅ Regular price → sanitize_number() ✅ Sale price → sanitize_number() ✅ Weight → sanitize_number() ✅ Length → sanitize_number() ✅ Width → sanitize_number() ✅ Height → sanitize_number() 3. Fixed update_product() (Lines 377-398): ✅ Same sanitization as create ✅ All text fields sanitized ✅ All numeric fields sanitized ✅ Status fields use sanitize_key() Sanitization Logic: Text Fields: - sanitize_text_field() + trim() - Prevents XSS attacks - Example: '' → '' Textarea Fields: - sanitize_textarea_field() + trim() - Allows newlines for descriptions - Prevents XSS but keeps formatting Numbers: - Remove non-numeric except . and - - Example: 'abc123.45' → '123.45' - Example: '10,000' → '10000' Slugs: - sanitize_title() - Creates URL-safe slugs - Example: 'Product Name!' → 'product-name' Module Audit Results: ✅ Orders: FIXED (comprehensive sanitization) ✅ Coupons: GOOD (already has sanitization) ✅ Customers: GOOD (already has sanitization) ✅ Products: FIXED (added comprehensive sanitization) All modules now have consistent, secure data handling! --- includes/Api/ProductsController.php | 100 ++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 28 deletions(-) diff --git a/includes/Api/ProductsController.php b/includes/Api/ProductsController.php index 6c15f5d..3882452 100644 --- a/includes/Api/ProductsController.php +++ b/includes/Api/ProductsController.php @@ -20,6 +20,50 @@ use WC_Product_Variation; class ProductsController { + /** + * Sanitize text field + */ + private static function sanitize_text($value) { + if (!isset($value) || $value === '') { + return ''; + } + $sanitized = sanitize_text_field($value); + return trim($sanitized); + } + + /** + * Sanitize textarea (allows newlines) + */ + private static function sanitize_textarea($value) { + if (!isset($value) || $value === '') { + return ''; + } + $sanitized = sanitize_textarea_field($value); + return trim($sanitized); + } + + /** + * Sanitize numeric value + */ + private static function sanitize_number($value) { + if (!isset($value) || $value === '') { + return ''; + } + // Remove non-numeric except decimal point and minus + $sanitized = preg_replace('/[^0-9.-]/', '', $value); + return $sanitized !== '' ? $sanitized : ''; + } + + /** + * Sanitize slug + */ + private static function sanitize_slug($value) { + if (!isset($value) || $value === '') { + return ''; + } + return sanitize_title($value); + } + /** * Register REST API routes */ @@ -231,24 +275,24 @@ class ProductsController { $product = new WC_Product_Simple(); } - // Set basic data - $product->set_name($data['name']); + // Set basic data - sanitize all inputs + $product->set_name(self::sanitize_text($data['name'])); if (!empty($data['slug'])) { - $product->set_slug($data['slug']); + $product->set_slug(self::sanitize_slug($data['slug'])); } - $product->set_status($data['status'] ?? 'publish'); - $product->set_description($data['description'] ?? ''); - $product->set_short_description($data['short_description'] ?? ''); + $product->set_status(sanitize_key($data['status'] ?? 'publish')); + $product->set_description(self::sanitize_textarea($data['description'] ?? '')); + $product->set_short_description(self::sanitize_textarea($data['short_description'] ?? '')); if (!empty($data['sku'])) { - $product->set_sku($data['sku']); + $product->set_sku(self::sanitize_text($data['sku'])); } if (!empty($data['regular_price'])) { - $product->set_regular_price($data['regular_price']); + $product->set_regular_price(self::sanitize_number($data['regular_price'])); } if (!empty($data['sale_price'])) { - $product->set_sale_price($data['sale_price']); + $product->set_sale_price(self::sanitize_number($data['sale_price'])); } $product->set_manage_stock($data['manage_stock'] ?? false); @@ -258,18 +302,18 @@ class ProductsController { } $product->set_stock_status($data['stock_status'] ?? 'instock'); - // Optional fields + // Optional fields - sanitize dimensions if (!empty($data['weight'])) { - $product->set_weight($data['weight']); + $product->set_weight(self::sanitize_number($data['weight'])); } if (!empty($data['length'])) { - $product->set_length($data['length']); + $product->set_length(self::sanitize_number($data['length'])); } if (!empty($data['width'])) { - $product->set_width($data['width']); + $product->set_width(self::sanitize_number($data['width'])); } if (!empty($data['height'])) { - $product->set_height($data['height']); + $product->set_height(self::sanitize_number($data['height'])); } // Virtual and downloadable @@ -330,15 +374,15 @@ class ProductsController { return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]); } - // Update basic data - if (isset($data['name'])) $product->set_name($data['name']); - if (isset($data['slug'])) $product->set_slug($data['slug']); - if (isset($data['status'])) $product->set_status($data['status']); - if (isset($data['description'])) $product->set_description($data['description']); - if (isset($data['short_description'])) $product->set_short_description($data['short_description']); - if (isset($data['sku'])) $product->set_sku($data['sku']); - if (isset($data['regular_price'])) $product->set_regular_price($data['regular_price']); - if (isset($data['sale_price'])) $product->set_sale_price($data['sale_price']); + // Update basic data - sanitize all inputs + if (isset($data['name'])) $product->set_name(self::sanitize_text($data['name'])); + if (isset($data['slug'])) $product->set_slug(self::sanitize_slug($data['slug'])); + if (isset($data['status'])) $product->set_status(sanitize_key($data['status'])); + if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description'])); + if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description'])); + if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku'])); + if (isset($data['regular_price'])) $product->set_regular_price(self::sanitize_number($data['regular_price'])); + if (isset($data['sale_price'])) $product->set_sale_price(self::sanitize_number($data['sale_price'])); if (isset($data['manage_stock'])) { $product->set_manage_stock($data['manage_stock']); @@ -347,11 +391,11 @@ class ProductsController { } } - if (isset($data['stock_status'])) $product->set_stock_status($data['stock_status']); - if (isset($data['weight'])) $product->set_weight($data['weight']); - if (isset($data['length'])) $product->set_length($data['length']); - if (isset($data['width'])) $product->set_width($data['width']); - if (isset($data['height'])) $product->set_height($data['height']); + if (isset($data['stock_status'])) $product->set_stock_status(sanitize_key($data['stock_status'])); + if (isset($data['weight'])) $product->set_weight(self::sanitize_number($data['weight'])); + if (isset($data['length'])) $product->set_length(self::sanitize_number($data['length'])); + if (isset($data['width'])) $product->set_width(self::sanitize_number($data['width'])); + if (isset($data['height'])) $product->set_height(self::sanitize_number($data['height'])); // Categories if (isset($data['categories'])) {