Commit ecdb1146e05 for woocommerce

commit ecdb1146e05bd8022b17d9f4fffa4c48d5b05bd4
Author: Darren Ethier <darren@roughsmootheng.in>
Date:   Thu Sep 3 10:52:37 2026 -0400

    Reject array order keys on pay, cancel and PayPal return paths (#68307)

    An array-valued order key reached hash_equals() on the order pay,
    cancel and PayPal return paths and raised an uncaught TypeError on
    PHP 8, turning a bad link into a fatal error. Treat a non-string key
    as absent so those paths fall through to their existing invalid-order
    handling. Follows the same guard added for the order received page.

diff --git a/plugins/woocommerce/changelog/fix-wooplug-7659-order-key-array-guards b/plugins/woocommerce/changelog/fix-wooplug-7659-order-key-array-guards
new file mode 100644
index 00000000000..964c0ecbcb8
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7659-order-key-array-guards
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Treat array-valued order keys as invalid on the order pay, cancel and PayPal return paths instead of raising a PHP error.
diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php
index 1e964a9bdf9..fb942e5200f 100644
--- a/plugins/woocommerce/includes/class-wc-form-handler.php
+++ b/plugins/woocommerce/includes/class-wc-form-handler.php
@@ -497,7 +497,7 @@ class WC_Form_Handler {
 			ob_start();

 			// Pay for existing order.
-			$order_key = wp_unslash( $_GET['key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			$order_key = is_string( $_GET['key'] ) ? wp_unslash( $_GET['key'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 			$order_id  = absint( $wp->query_vars['order-pay'] );
 			$order     = wc_get_order( $order_id );

@@ -866,7 +866,7 @@ class WC_Form_Handler {
 		) {
 			wc_nocache_headers();

-			$order_key = wp_unslash( $_GET['order'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			$order_key = is_string( $_GET['order'] ) ? wp_unslash( $_GET['order'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 			$order_id  = absint( $_GET['order_id'] );
 			$order     = wc_get_order( $order_id );
 			/**
diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php
index 46ef28752da..b8d251ec3c7 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php
@@ -34,7 +34,7 @@ abstract class WC_Gateway_Paypal_Response {
 		$custom = json_decode( $raw_custom );
 		if ( $custom && is_object( $custom ) ) {
 			$order_id  = $custom->order_id;
-			$order_key = $custom->order_key;
+			$order_key = is_string( $custom->order_key ) ? $custom->order_key : '';
 		} else {
 			// Nothing was found.
 			WC_Gateway_Paypal::log( 'Order ID and key were not found in "custom".', 'error' );
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php
index 098997e2d97..7e9b6be4b7f 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php
@@ -87,7 +87,7 @@ class WC_Shortcode_Checkout {
 		// Pay for existing order.
 		if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flow selectors are cleaned; order access verifies key/ownership.
 			try {
-				$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flow selectors are cleaned; order access verifies key/ownership.
+				$order_key = isset( $_GET['key'] ) && is_string( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flow selectors are cleaned; order access verifies key/ownership.
 				$order     = wc_get_order( $order_id );

 				// Order or payment link is invalid.
@@ -233,7 +233,7 @@ class WC_Shortcode_Checkout {
 		} elseif ( $order_id ) {

 			// Pay for order after checkout step.
-			$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flow selectors are cleaned; order access verifies key/ownership.
+			$order_key = isset( $_GET['key'] ) && is_string( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flow selectors are cleaned; order access verifies key/ownership.
 			$order     = wc_get_order( $order_id );

 			if ( $order && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) {
diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php
index 855ef8d3533..ed08cc45e0c 100644
--- a/plugins/woocommerce/includes/wc-cart-functions.php
+++ b/plugins/woocommerce/includes/wc-cart-functions.php
@@ -183,7 +183,7 @@ function wc_clear_cart_after_payment() {
 	if ( ! empty( $wp->query_vars['order-received'] ) ) {

 		$order_id  = absint( $wp->query_vars['order-received'] );
-		$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Raw referer is sanitized and validated by its consumers.
+		$order_key = isset( $_GET['key'] ) && is_string( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Order key is cleaned and compared with hash_equals() against the order's key below.

 		if ( $order_id > 0 ) {
 			$order = wc_get_order( $order_id );
diff --git a/plugins/woocommerce/tests/php/includes/shortcodes/class-wc-shortcode-checkout-test.php b/plugins/woocommerce/tests/php/includes/shortcodes/class-wc-shortcode-checkout-test.php
index 246e7569fb7..52148ebce74 100644
--- a/plugins/woocommerce/tests/php/includes/shortcodes/class-wc-shortcode-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/shortcodes/class-wc-shortcode-checkout-test.php
@@ -19,7 +19,9 @@ class WC_Shortcode_Checkout_Test extends WC_Unit_Test_Case {
 		global $wp;

 		unset( $_GET['key'] );
+		unset( $_GET['pay_for_order'] );
 		unset( $wp->query_vars['order-received'] );
+		unset( $wp->query_vars['order-pay'] );

 		parent::tearDown();
 	}
@@ -42,4 +44,23 @@ class WC_Shortcode_Checkout_Test extends WC_Unit_Test_Case {
 		$this->assertStringNotContainsString( 'woocommerce-thankyou-order-details', $output );
 		$this->assertStringNotContainsString( (string) $order->get_order_number(), $output );
 	}
+
+	/**
+	 * An array `key` on the pay-for-order page must be rejected rather than reaching hash_equals().
+	 */
+	public function test_order_pay_treats_array_order_key_as_invalid() {
+		global $wp;
+
+		$order = WC_Helper_Order::create_order( 0 );
+
+		$wp->query_vars['order-pay'] = $order->get_id();
+		$_GET['pay_for_order']       = 'true';
+		$_GET['key']                 = array( $order->get_order_key() );
+
+		ob_start();
+		WC_Shortcode_Checkout::output( array() );
+		$output = (string) ob_get_clean();
+
+		$this->assertStringContainsString( 'Sorry, this order is invalid and cannot be paid for.', $output );
+	}
 }