Commit c67de74409c for woocommerce

commit c67de74409cf4c6a3523506062d0498236919330
Author: Anuj Singh <80690679+Anuj-Rathore24@users.noreply.github.com>
Date:   Tue Mar 10 17:22:22 2026 +0530

    fix: allow math expressions in flat rate shipping cost field. (#63453)

diff --git a/plugins/woocommerce/changelog/fix-flat-rate-shipping-math-expression-validation b/plugins/woocommerce/changelog/fix-flat-rate-shipping-math-expression-validation
new file mode 100644
index 00000000000..88b75cdb8b5
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-flat-rate-shipping-math-expression-validation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix math expressions being rejected in the flat rate shipping cost field
diff --git a/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php b/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php
index 7b63514e98d..f3ad1192c03 100644
--- a/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php
+++ b/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php
@@ -273,6 +273,32 @@ class WC_Shipping_Flat_Rate extends WC_Shipping_Method {
 		return $found_shipping_classes;
 	}

+	/**
+	 * Check if a value is a math expression.
+	 *
+	 * Matches two or more numeric operands separated by arithmetic operators.
+	 *
+	 * @param string $value Value to test.
+	 * @return bool True if value is a well-formed math expression.
+	 */
+	protected function is_math_expression( string $value ): bool {
+		$decimal_separator = wc_get_price_decimal_separator();
+
+		// Use preg_quote() to safely escaping separator.
+		$separator = preg_quote( $decimal_separator, '/' );
+
+		// Breakdown:
+		// [\d{sep}\s]+             - First operand: digits, separator, or spaces.
+		// (                        - Begin repeating group:
+		// [+\-*\/]               -   An arithmetic operator (+, -, *, /).
+		// [\d{sep}\s]+           -   Next operand: digits, separator, or spaces.
+		// )+                       - One or more operator+operand pairs (ensures no trailing operator).
+		return (bool) preg_match(
+			'/^[\d' . $separator . '\s]+([+\-*\/][\d' . $separator . '\s]+)+$/',
+			trim( $value )
+		);
+	}
+
 	/**
 	 * Sanitize the cost field.
 	 *
@@ -287,7 +313,8 @@ class WC_Shipping_Flat_Rate extends WC_Shipping_Method {
 		$value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value );

 		$contains_shortcodes = false !== strpos( $value, '[' ) || false !== strpos( $value, ']' );
-		if ( ! $contains_shortcodes ) {
+
+		if ( ! $contains_shortcodes && ! $this->is_math_expression( $value ) ) {
 			$value = \Automattic\WooCommerce\Utilities\NumberUtil::sanitize_cost_in_current_locale( $value );
 		}

diff --git a/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php b/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php
index 5ff8c925575..7d8fcffe831 100644
--- a/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php
+++ b/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php
@@ -1,4 +1,5 @@
 <?php
+declare( strict_types = 1 );

 // phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- backcompat nomenclature.

@@ -17,6 +18,11 @@ class WC_Shipping_Flat_Rate_Test extends WC_Unit_Test_Case {
 	 */
 	private $call_evaluate_cost;

+	/**
+	 * @var Closure Function to call public method sanitize_cost.
+	 */
+	private $call_sanitize_cost;
+
 	/**
 	 * Set up test case.
 	 *
@@ -28,6 +34,9 @@ class WC_Shipping_Flat_Rate_Test extends WC_Unit_Test_Case {
 		$this->call_evaluate_cost = function ( $sum, $args ) {
 			return $this->evaluate_cost( $sum, $args );
 		};
+		$this->call_sanitize_cost = function ( $value ) {
+			return $this->sanitize_cost( $value );
+		};
 		update_option( 'woocommerce_price_decimal_sep', ',' );
 		update_option( 'woocommerce_price_thousand_sep', '.' );
 	}
@@ -151,4 +160,92 @@ class WC_Shipping_Flat_Rate_Test extends WC_Unit_Test_Case {
 		);
 		$this->assertEquals( 10, $val );
 	}
+
+	/**
+	 * @testDox sanitize_cost() accepts and preserves valid math expressions.
+	 *
+	 * @dataProvider provider_valid_math_expressions
+	 *
+	 * @param string $value           Value to test.
+	 * @param string $decimal_sep     Decimal separator to use.
+	 * @param string $thousand_sep    Thousand separator to use.
+	 */
+	public function test_sanitize_cost_accepts_math_expressions( string $value, string $decimal_sep, string $thousand_sep ): void {
+		update_option( 'woocommerce_price_decimal_sep', $decimal_sep );
+		update_option( 'woocommerce_price_thousand_sep', $thousand_sep );
+
+		$result = $this->call_sanitize_cost->call( $this->sut, $value );
+		$this->assertEquals( $value, trim( $result ) );
+	}
+
+	/**
+	 * @testDox sanitize_cost() rejects invalid math expressions.
+	 *
+	 * @dataProvider provider_invalid_math_expressions
+	 *
+	 * @param string $value       Value to sanitize.
+	 * @param string $decimal_sep Decimal separator to use.
+	 * @param string $thousand_sep Thousand separator to use.
+	 */
+	public function test_sanitize_cost_rejects_invalid_expressions( string $value, string $decimal_sep, string $thousand_sep ): void {
+		update_option( 'woocommerce_price_decimal_sep', $decimal_sep );
+		update_option( 'woocommerce_price_thousand_sep', $thousand_sep );
+
+		$this->expectException( Exception::class );
+		$this->call_sanitize_cost->call( $this->sut, $value );
+	}
+
+	/**
+	 * Valid math expression cases.
+	 *
+	 * Format: [ value, decimal_separator, thousand_separator ]
+	 */
+	public function provider_valid_math_expressions(): array {
+		return array(
+			'plain number'                  => array( '10.00', '.', ',' ),
+			'empty string'                  => array( '', '.', ',' ),
+			'shortcode qty'                 => array( '[qty]', '.', ',' ),
+			'shortcode expression'          => array( '10.00 * [qty]', '.', ',' ),
+
+			// period decimal, comma thousand.
+			'simple division'               => array( '3.50 / 1.21', '.', ',' ),
+			'simple multiplication'         => array( '10.00 * 1.21', '.', ',' ),
+			'simple addition'               => array( '10 + 5', '.', ',' ),
+			'simple subtraction'            => array( '20 - 3.50', '.', ',' ),
+			'chained operators'             => array( '10 * 2 + 5', '.', ',' ),
+
+			// comma decimal, period thousand.
+			'EU locale division'            => array( '3,50 / 1,21', ',', '.' ),
+			'EU locale multiplication'      => array( '10,00 * 1,21', ',', '.' ),
+
+			// No thousand separator locale.
+			'no thousand separator simple'  => array( '3.50 / 1.21', '.', '' ),
+			'no thousand separator chained' => array( '10 * 2 + 5', '.', '' ),
+		);
+	}
+
+	/**
+	 * Invalid math expression cases.
+	 *
+	 * Format: [ value, decimal_separator, thousand_separator ]
+	 */
+	public function provider_invalid_math_expressions(): array {
+		return array(
+			// Thousand-separated operands must not be used in math expressions
+			// as evaluate_cost() normalises all separators to ".", causing
+			// "10,000" to be evaluated as "10.0" instead of "10000".
+			'thousand separated operand'    => array( '10,500 * 3000', '.', ',' ),
+			'EU thousand separated operand' => array( '10.500 * 3000', ',', '.' ),
+
+			// Trailing operator — incomplete expressions.
+			'trailing plus'                 => array( '20 +', '.', ',' ),
+			'trailing minus'                => array( '20 -', '.', ',' ),
+			'trailing multiply'             => array( '20 *', '.', ',' ),
+			'trailing divide'               => array( '3.50 /', '.', ',' ),
+
+			// Invalid characters.
+			'alphabetic string'             => array( 'abc', '.', ',' ),
+			'alphanumeric'                  => array( '10abc', '.', ',' ),
+		);
+	}
 }