Commit 15820b579ac for woocommerce

commit 15820b579ac9fa52830ad09f7124b8e664c91df9
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Wed Jul 22 16:40:13 2026 +0100

    Add check to background Stripe installer redirect (#66880)

diff --git a/plugins/woocommerce/changelog/fix-stripe-install-redirect-csrf b/plugins/woocommerce/changelog/fix-stripe-install-redirect-csrf
new file mode 100644
index 00000000000..4a8cbcd4717
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-stripe-install-redirect-csrf
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Tighten authorization checks on the Stripe extension install redirect.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin.php b/plugins/woocommerce/includes/admin/class-wc-admin.php
index 829c938b72d..5d4bd856372 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin.php
@@ -152,12 +152,12 @@ class WC_Admin {
 			return;
 		}

-		// phpcs:disable WordPress.Security.NonceVerification.Recommended
 		// Nonced plugin install redirects.
 		if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) {
-			$plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) );
+			$plugin_slug    = is_string( $_GET['wc-install-plugin-redirect'] ) ? sanitize_text_field( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ) : '';
+			$redirect_nonce = isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';

-			if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) {
+			if ( wp_verify_nonce( $redirect_nonce, 'wc-install-plugin-redirect_' . $plugin_slug ) && current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) {
 				$nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug );
 				$url   = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce );
 			} else {
@@ -167,7 +167,6 @@ class WC_Admin {
 			wp_safe_redirect( $url );
 			exit;
 		}
-		// phpcs:enable WordPress.Security.NonceVerification.Recommended
 	}

 	/**
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 9372e5b4be5..755c6e07ec7 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -2644,10 +2644,13 @@ $email_unsubscribes_table_schema;
 	 *
 	 * @throws Exception If unable to proceed with plugin installation.
 	 * @since  2.6.0
+	 * @deprecated 11.1.0 No longer used.
 	 *
 	 * @return void
 	 */
 	public static function background_installer( $plugin_to_install_id, $plugin_to_install ) {
+		wc_deprecated_function( 'WC_Install::background_installer', '11.1.0' );
+
 		// Explicitly clear the event.
 		$args = func_get_args();

@@ -2749,7 +2752,12 @@ $email_unsubscribes_table_schema;
 							__( '%1$s could not be installed (%2$s). <a href="%3$s">Please install it manually by clicking here.</a>', 'woocommerce' ),
 							$plugin_to_install['name'],
 							$e->getMessage(),
-							esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ) )
+							esc_url(
+								wp_nonce_url(
+									admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ),
+									'wc-install-plugin-redirect_' . $plugin_slug
+								)
+							)
 						)
 					);
 				}
@@ -2807,10 +2815,13 @@ $email_unsubscribes_table_schema;
 	 *
 	 * @throws Exception If unable to proceed with theme installation.
 	 * @since  3.1.0
+	 * @deprecated 11.1.0 No longer used.
 	 *
 	 * @return void
 	 */
 	public static function theme_background_installer( $theme_slug ) {
+		wc_deprecated_function( 'WC_Install::theme_background_installer', '11.1.0' );
+
 		// Explicitly clear the event.
 		$args = func_get_args();

diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 14a946dc738..770f1917c48 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -3282,12 +3282,6 @@ parameters:
 			count: 1
 			path: includes/admin/class-wc-admin-webhooks.php

-		-
-			message: '#^Binary operation "\." between ''plugin\-install\.php…'' and array\|string results in an error\.$#'
-			identifier: binaryOp.invalid
-			count: 1
-			path: includes/admin/class-wc-admin.php
-
 		-
 			message: '#^Method WC_Admin\:\:admin_redirects\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php
new file mode 100644
index 00000000000..4c5b40d2f81
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php
@@ -0,0 +1,104 @@
+<?php
+declare( strict_types = 1 );
+
+/**
+ * Tests for the WC_Admin class.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+
+/**
+ * WC_Admin_Test
+ */
+class WC_Admin_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * System under test.
+	 *
+	 * @var WC_Admin
+	 */
+	private WC_Admin $sut;
+
+	/**
+	 * Original $_GET.
+	 *
+	 * @var array<string,mixed>
+	 */
+	private array $original_get = array();
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->sut          = new WC_Admin();
+		$this->original_get = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		add_filter( 'wp_redirect', array( $this, 'intercept_redirect' ) );
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		remove_filter( 'wp_redirect', array( $this, 'intercept_redirect' ) );
+		$_GET = $this->original_get; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		wp_set_current_user( 0 );
+		parent::tearDown();
+	}
+
+	/**
+	 * Intercepts redirects so the tested handler's trailing exit does not run.
+	 *
+	 * @param string $location Redirect target.
+	 * @return never
+	 * @throws RuntimeException Always.
+	 */
+	public function intercept_redirect( string $location ): void {
+		throw new RuntimeException( esc_url_raw( $location ) );
+	}
+
+	/**
+	 * @testdox admin_redirects() only triggers the plugin install with a valid nonce for an allowed plugin slug.
+	 */
+	public function test_install_plugin_redirect_requires_valid_nonce(): void {
+		$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+		wp_set_current_user( $admin_id );
+
+		// Missing/invalid nonce: falls back to the search page.
+		$_GET = array( // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			'wc-install-plugin-redirect' => 'woocommerce-gateway-stripe',
+			'_wpnonce'                   => 'not-a-valid-nonce',
+		);
+		try {
+			$this->sut->admin_redirects();
+			$this->fail( 'Expected the redirect interception to throw.' );
+		} catch ( RuntimeException $e ) {
+			$this->assertStringNotContainsString( 'action=install-plugin', $e->getMessage(), 'Invalid nonce.' );
+		}
+
+		// Disallowed plugin slug, even with a matching valid nonce: also falls back.
+		$_GET = array( // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			'wc-install-plugin-redirect' => 'some-other-plugin',
+			'_wpnonce'                   => wp_create_nonce( 'wc-install-plugin-redirect_some-other-plugin' ),
+		);
+		try {
+			$this->sut->admin_redirects();
+			$this->fail( 'Expected the redirect interception to throw.' );
+		} catch ( RuntimeException $e ) {
+			$this->assertStringNotContainsString( 'action=install-plugin', $e->getMessage(), 'Disallowed plugin slug.' );
+		}
+
+		// Valid, matching nonce for the allowed plugin: triggers the install.
+		$_GET = array( // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			'wc-install-plugin-redirect' => 'woocommerce-gateway-stripe',
+			'_wpnonce'                   => wp_create_nonce( 'wc-install-plugin-redirect_woocommerce-gateway-stripe' ),
+		);
+		try {
+			$this->sut->admin_redirects();
+			$this->fail( 'Expected the redirect interception to throw.' );
+		} catch ( RuntimeException $e ) {
+			$this->assertStringContainsString( 'action=install-plugin', $e->getMessage() );
+			$this->assertStringContainsString( 'plugin=woocommerce-gateway-stripe', $e->getMessage() );
+		}
+	}
+}