Commit 1e451066d8b for woocommerce

commit 1e451066d8bf87e099c3796039fffe66bd16a1c0
Author: Darren Ethier <darren@roughsmootheng.in>
Date:   Wed Sep 2 14:06:01 2026 -0400

    Reject array wc-auth parameters and replace legacy WPCS suppressions (#68270)

diff --git a/plugins/woocommerce/changelog/fix-woo6-113-auth-callback-suppressions b/plugins/woocommerce/changelog/fix-woo6-113-auth-callback-suppressions
new file mode 100644
index 00000000000..67f9ed23072
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woo6-113-auth-callback-suppressions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Reject array values for the wc-auth handshake parameters so they no longer cause a fatal error, and replace legacy WPCS suppression comments in the auth, REST authentication and PayPal IPN handlers with exact PHPCS annotations.
diff --git a/plugins/woocommerce/includes/class-wc-auth.php b/plugins/woocommerce/includes/class-wc-auth.php
index 2f33efebb63..27144bf7ccb 100644
--- a/plugins/woocommerce/includes/class-wc-auth.php
+++ b/plugins/woocommerce/includes/class-wc-auth.php
@@ -167,12 +167,18 @@ class WC_Auth {
 		);

 		foreach ( $params as $param ) {
-			if ( empty( $_REQUEST[ $param ] ) ) { // WPCS: input var ok, CSRF ok.
+			if ( empty( $_REQUEST[ $param ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Values are required for handshake validation; key creation verifies capability and nonce.
 				/* translators: %s: parameter */
 				throw new Exception( sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param ) );
 			}

-			$data[ $param ] = wp_unslash( $_REQUEST[ $param ] ); // WPCS: input var ok, CSRF ok, sanitization ok.
+			// Every handshake parameter is a single string; anything else, an array in particular, is rejected here so that it cannot reach the string-only functions used below.
+			if ( ! is_string( $_REQUEST[ $param ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Values are required for handshake validation; key creation verifies capability and nonce.
+				/* translators: %s: parameter */
+				throw new Exception( sprintf( __( 'Invalid parameter %s', 'woocommerce' ), $param ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- The caught message is escaped by esc_html() where it is rendered; escaping here would double-encode translated text.
+			}
+
+			$data[ $param ] = wp_unslash( $_REQUEST[ $param ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw values are needed to validate the submitted URLs and scope; each value is sanitized or escaped where it is used, and key creation verifies capability and nonce.
 		}

 		if ( ! in_array( $data['scope'], array( 'read', 'write', 'read_write' ), true ) ) {
@@ -320,7 +326,8 @@ class WC_Auth {
 			$route = strtolower( wc_clean( $route ) );
 			$this->make_validation();

-			$data = wp_unslash( $_REQUEST ); // WPCS: input var ok, CSRF ok.
+			// Validated by make_validation() above. Values are cleaned or escaped where they are rendered or stored; create_keys() sanitizes what it persists.
+			$data = wp_unslash( $_REQUEST );

 			// Login endpoint.
 			if ( 'login' === $route && ! is_user_logged_in() ) {
diff --git a/plugins/woocommerce/includes/class-wc-rest-authentication.php b/plugins/woocommerce/includes/class-wc-rest-authentication.php
index ddc8ac1d82e..07a08d0bf53 100644
--- a/plugins/woocommerce/includes/class-wc-rest-authentication.php
+++ b/plugins/woocommerce/includes/class-wc-rest-authentication.php
@@ -454,7 +454,7 @@ class WC_REST_Authentication {
 	 * @return array|WP_Error
 	 */
 	public function get_oauth_parameters() {
-		$params = array_merge( $_GET, $_POST ); // WPCS: CSRF ok.
+		$params = array_merge( $_GET, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Raw credentials and request data are required for OAuth signature verification.
 		$params = wp_unslash( $params );
 		$header = $this->get_authorization_header();

diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php
index d92c7ec3fc5..a70d70416e1 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php
@@ -45,8 +45,8 @@ class WC_Gateway_Paypal_IPN_Handler extends WC_Gateway_Paypal_Response {
 	 * Check for PayPal IPN Response.
 	 */
 	public function check_response() {
-		if ( ! empty( $_POST ) && $this->validate_ipn() ) { // WPCS: CSRF ok.
-			$posted = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.
+		if ( ! empty( $_POST ) && $this->validate_ipn() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- PayPal posts the IPN from its own servers, so no site nonce exists; validate_ipn() echoes the payload back to PayPal and only a VERIFIED response allows processing to continue.
+			$posted = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- PayPal posts the IPN from its own servers, so no site nonce exists; the payload is only read after validate_ipn() confirms it, and each value is sanitized where it is consumed.

 			// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
 			do_action( 'valid-paypal-standard-ipn-request', $posted );
@@ -85,7 +85,7 @@ class WC_Gateway_Paypal_IPN_Handler extends WC_Gateway_Paypal_Response {
 		WC_Gateway_Paypal::log( 'Checking IPN response is valid' );

 		// Get received values from post data.
-		$validate_ipn        = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.
+		$validate_ipn        = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The unmodified payload must be echoed back to PayPal for verification, so no site nonce applies and the values cannot be altered here.
 		$validate_ipn['cmd'] = '_notify-validate';

 		// Send back post vars to paypal.
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-auth-test.php b/plugins/woocommerce/tests/php/includes/class-wc-auth-test.php
index 5f6907fec3e..96a378e8c53 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-auth-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-auth-test.php
@@ -35,4 +35,40 @@ class WC_Auth_Test extends \WC_Unit_Test_Case {
 		$maybe_delete_key->setAccessible( true );
 		$maybe_delete_key->invoke( $wc_auth, $key_data );
 	}
+
+	/**
+	 * Get the protected WC_Auth::make_validation() method.
+	 *
+	 * @return ReflectionMethod
+	 */
+	private function get_make_validation_method() {
+		$make_validation = ( new ReflectionClass( WC_Auth::class ) )->getMethod( 'make_validation' );
+		$make_validation->setAccessible( true );
+
+		return $make_validation;
+	}
+
+	/**
+	 * An array submitted for a handshake parameter is reported as invalid rather than missing, and never
+	 * reaches a string-only function.
+	 */
+	public function test_make_validation_rejects_array_parameters() {
+		$request_backup = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Saved and restored so the test can drive make_validation() with a known request.
+		$_REQUEST       = array(
+			'app_name'     => 'Test app',
+			'user_id'      => '123',
+			'return_url'   => array( 'https://example.com/return' ),
+			'callback_url' => 'https://example.com/callback',
+			'scope'        => 'read',
+		);
+
+		$this->expectException( Exception::class );
+		$this->expectExceptionMessage( 'Invalid parameter return_url' );
+
+		try {
+			$this->get_make_validation_method()->invoke( new WC_Auth() );
+		} finally {
+			$_REQUEST = $request_backup;
+		}
+	}
 }