Commit 26f01dec553 for woocommerce

commit 26f01dec553f3868c674f4ef605c63606975c5ea
Author: James Kemp <me@jckemp.com>
Date:   Tue Nov 10 09:00:48 2026 +0000

    Fix My Account login silently failing with a stale session cookie (#70785)

    * Fix My Account login silently failing with a stale session cookie

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Address review feedback

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/70785-fix-70762-my-account-login-silently-fails-page b/plugins/woocommerce/changelog/70785-fix-70762-my-account-login-silently-fails-page
new file mode 100644
index 00000000000..093cd518573
--- /dev/null
+++ b/plugins/woocommerce/changelog/70785-fix-70762-my-account-login-silently-fails-page
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix My Account login and registration silently failing when a shopper's session cookie can no longer be verified after the site's security keys change.
diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php
index 437e7c16815..60a22e51e5a 100644
--- a/plugins/woocommerce/includes/class-wc-session-handler.php
+++ b/plugins/woocommerce/includes/class-wc-session-handler.php
@@ -166,6 +166,13 @@ class WC_Session_Handler extends WC_Session {
 		$cookie = $this->get_session_cookie();

 		if ( ! $cookie ) {
+			// A cookie that is present but cannot be verified (e.g. the site's salts changed) is not a session.
+			// Expire it and drop it from this request so has_session() doesn't treat it as one.
+			if ( isset( $_COOKIE[ $this->_cookie ] ) ) {
+				wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true );
+				unset( $_COOKIE[ $this->_cookie ] );
+			}
+
 			// If there is no cookie, generate a new session/customer ID.
 			$this->_customer_id = $this->generate_customer_id();
 			$this->_data        = $this->get_session_data();
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-session-concurrency-test.php b/plugins/woocommerce/tests/php/includes/class-wc-session-concurrency-test.php
index 999942388a0..f14e2db00cf 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-session-concurrency-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-session-concurrency-test.php
@@ -146,6 +146,78 @@ class WC_Session_Concurrency_Test extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox A session cookie that fails verification is dropped from the request, so the visitor has no session.
+	 */
+	public function test_unverifiable_session_cookie_is_not_treated_as_a_session(): void {
+		$cookie_name             = 'wp_woocommerce_session_' . COOKIEHASH;
+		$_COOKIE[ $cookie_name ] = 't_0123456789abcdef0123456789abcd|' . ( time() + DAY_IN_SECONDS ) . '|' . ( time() + HOUR_IN_SECONDS ) . '|invalidhash';
+
+		// Avoid sending headers from the test process.
+		add_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+		try {
+			$handler = new WC_Session_Handler();
+			$handler->init_session_cookie();
+
+			$this->assertArrayNotHasKey( $cookie_name, $_COOKIE, 'The unverifiable cookie should be removed from the request' );
+			$this->assertFalse( $handler->has_session(), 'An unverifiable cookie should not count as a session' );
+		} finally {
+			remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+			unset( $_COOKIE[ $cookie_name ] );
+		}
+	}
+
+	/**
+	 * @testdox A validly signed session cookie is kept in the request and counts as a session.
+	 */
+	public function test_verifiable_session_cookie_is_kept_as_a_session(): void {
+		$cookie_name = 'wp_woocommerce_session_' . COOKIEHASH;
+		$customer_id = 't_0123456789abcdef0123456789abcd';
+		$expiration  = time() + DAY_IN_SECONDS;
+		$expiring    = time() + HOUR_IN_SECONDS;
+		$message     = $customer_id . '|' . $expiration;
+		$hash        = hash_hmac( 'md5', $message, wp_hash( $message ) );
+
+		$_COOKIE[ $cookie_name ] = $message . '|' . $expiring . '|' . $hash;
+
+		// Avoid sending headers from the test process.
+		add_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+		try {
+			$handler = new WC_Session_Handler();
+			$handler->init_session_cookie();
+
+			$this->assertArrayHasKey( $cookie_name, $_COOKIE, 'A verifiable cookie should stay in the request' );
+			$this->assertTrue( $handler->has_session(), 'A verifiable cookie should count as a session' );
+		} finally {
+			remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+			unset( $_COOKIE[ $cookie_name ] );
+		}
+	}
+
+	/**
+	 * @testdox A session cookie with the wrong number of parts is dropped from the request, so the visitor has no session.
+	 */
+	public function test_malformed_session_cookie_is_not_treated_as_a_session(): void {
+		$cookie_name             = 'wp_woocommerce_session_' . COOKIEHASH;
+		$_COOKIE[ $cookie_name ] = 't_0123456789abcdef0123456789abcd|' . ( time() + DAY_IN_SECONDS );
+
+		// Avoid sending headers from the test process.
+		add_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+		try {
+			$handler = new WC_Session_Handler();
+			$handler->init_session_cookie();
+
+			$this->assertArrayNotHasKey( $cookie_name, $_COOKIE, 'The malformed cookie should be removed from the request' );
+			$this->assertFalse( $handler->has_session(), 'A malformed cookie should not count as a session' );
+		} finally {
+			remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+			unset( $_COOKIE[ $cookie_name ] );
+		}
+	}
+
 	/**
 	 * Seeds the session, then interleaves two requests: load A, load B, save A, save B.
 	 *