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() );
+ }
+ }
+}