From 8d57884a705917984e4d484a965271abf25c39e4 Mon Sep 17 00:00:00 2001 From: roykho Date: Thu, 24 Feb 2022 08:49:11 -0800 Subject: [PATCH] Cherry picked sec changes --- .../includes/class-wc-checkout.php | 15 ++- .../includes/class-wc-session-handler.php | 74 ++++++++++++- .../class-wc-tests-session-handler.php | 101 +++++++++++++++++- 3 files changed, 185 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php index 076b8a48e32..9dfa2474b42 100644 --- a/plugins/woocommerce/includes/class-wc-checkout.php +++ b/plugins/woocommerce/includes/class-wc-checkout.php @@ -1134,9 +1134,19 @@ class WC_Checkout { */ public function process_checkout() { try { - $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore + $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore + $expiry_message = sprintf( + /* translators: %s: shop cart url */ + __( 'Sorry, your session has expired. Return to shop', 'woocommerce' ), + esc_url( wc_get_page_permalink( 'shop' ) ) + ); if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) { + // If the cart is empty, the nonce check failed because of session expiry. + if ( WC()->cart->is_empty() ) { + throw new Exception( $expiry_message ); + } + WC()->session->set( 'refresh_totals', true ); throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) ); } @@ -1147,8 +1157,7 @@ class WC_Checkout { do_action( 'woocommerce_before_checkout_process' ); if ( WC()->cart->is_empty() ) { - /* translators: %s: shop cart url */ - throw new Exception( sprintf( __( 'Sorry, your session has expired. Return to shop', 'woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ) ); + throw new Exception( $expiry_message ); } do_action( 'woocommerce_checkout_process' ); diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php index 0c6dda12484..a98bf04bc78 100644 --- a/plugins/woocommerce/includes/class-wc-session-handler.php +++ b/plugins/woocommerce/includes/class-wc-session-handler.php @@ -88,12 +88,18 @@ class WC_Session_Handler extends WC_Session { $cookie = $this->get_session_cookie(); if ( $cookie ) { + // Customer ID will be an MD5 hash id this is a guest session. $this->_customer_id = $cookie[0]; $this->_session_expiration = $cookie[1]; $this->_session_expiring = $cookie[2]; $this->_has_cookie = true; $this->_data = $this->get_session_data(); + if ( ! $this->is_session_cookie_valid() ) { + $this->destroy_session(); + $this->set_session_expiration(); + } + // If the user logs in, update session. if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) { $guest_session_id = $this->_customer_id; @@ -115,6 +121,30 @@ class WC_Session_Handler extends WC_Session { } } + /** + * Checks if session cookie is expired, or belongs to a logged out user. + * + * @return bool Whether session cookie is valid. + */ + private function is_session_cookie_valid() { + // If session is expired, session cookie is invalid. + if ( time() > $this->_session_expiration ) { + return false; + } + + // If user has logged out, session cookie is invalid. + if ( ! is_user_logged_in() && ! $this->is_customer_guest( $this->_customer_id ) ) { + return false; + } + + // Session from a different user is not valid. (Although from a guest user will be valid) + if ( is_user_logged_in() && ! $this->is_customer_guest( $this->_customer_id ) && strval( get_current_user_id() ) !== $this->_customer_id ) { + return false; + } + + return true; + } + /** * Sets the session cookie on-demand (usually after adding an item to the cart). * @@ -181,12 +211,54 @@ class WC_Session_Handler extends WC_Session { if ( empty( $customer_id ) ) { require_once ABSPATH . 'wp-includes/class-phpass.php'; $hasher = new PasswordHash( 8, false ); - $customer_id = md5( $hasher->get_random_bytes( 32 ) ); + $customer_id = 't_' . substr( md5( $hasher->get_random_bytes( 32 ) ), 2 ); } return $customer_id; } + /** + * Checks if this is an auto-generated customer ID. + * + * @param string|int $customer_id Customer ID to check. + * + * @return bool Whether customer ID is randomly generated. + */ + private function is_customer_guest( $customer_id ) { + $customer_id = strval( $customer_id ); + + if ( empty( $customer_id ) ) { + return true; + } + + if ( 't_' === substr( $customer_id, 0, 2 ) ) { + return true; + } + + /** + * Legacy checks. This is to handle sessions that were created from a previous release. + * Maybe we can get rid of them after a few releases. + */ + + // Almost all random $customer_ids will have some letters in it, while all actual ids will be integers. + if ( strval( (int) $customer_id ) !== $customer_id ) { + return true; + } + + // Performance hack to potentially save a DB query, when same user as $customer_id is logged in. + if ( is_user_logged_in() && strval( get_current_user_id() ) === $customer_id ) { + return false; + } else { + $customer = new WC_Customer( $customer_id ); + + if ( 0 === $customer->get_id() ) { + return true; + } + } + + return false; + } + /** * Get session unique ID for requests if session is initialized or user ID if logged in. * Introduced to help with unit tests. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php b/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php index 7af97b30165..bb60510722d 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php @@ -130,12 +130,111 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case { $this->assertEquals( $this->handler->get_customer_unique_id(), $this->handler->maybe_update_nonce_user_logged_out( 1, 'woocommerce-something' ) ); } + /** + * @testdox Test that session from cookie is destroyed if expired. + */ + public function test_destroy_session_cookie_expired() { + $customer_id = '1'; + $session_expiration = time() - 10000; + $session_expiring = time() - 1000; + $cookie_hash = ''; + $this->session_key = $customer_id; + + $handler = $this + ->getMockBuilder( WC_Session_Handler::class ) + ->setMethods( array( 'get_session_cookie' ) ) + ->getMock(); + + $handler + ->method( 'get_session_cookie' ) + ->willReturn( array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) ); + + add_filter( 'woocommerce_set_cookie_enabled', '__return_false' ); + + $handler->init_session_cookie(); + + remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' ); + + $this->assertFalse( wp_cache_get( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP ) ); + $this->assertNull( $this->get_session_from_db( $this->session_key ) ); + } + + /** + * @testdox Test that session from cookie is destroyed if user is logged out. + */ + public function test_destroy_session_user_logged_out() { + $customer_id = '1'; + $session_expiration = time() + 50000; + $session_expiring = time() + 5000; + $cookie_hash = ''; + $this->session_key = $customer_id; + + // Simulate a log out. + wp_set_current_user( 0 ); + + $handler = $this + ->getMockBuilder( WC_Session_Handler::class ) + ->setMethods( array( 'get_session_cookie' ) ) + ->getMock(); + + $handler + ->method( 'get_session_cookie' ) + ->willReturn( array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) ); + + add_filter( 'woocommerce_set_cookie_enabled', '__return_false' ); + + $handler->init_session_cookie(); + + remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' ); + + $this->assertFalse( wp_cache_get( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP ) ); + $this->assertNull( $this->get_session_from_db( $this->session_key ) ); + } + + /** + * @testdox Test that session from cookie is destroyed if logged in user doesn't match. + */ + public function test_destroy_session_user_mismatch() { + $customer = WC_Helper_Customer::create_customer(); + $customer_id = (string) $customer->get_id(); + $session_expiration = time() + 50000; + $session_expiring = time() + 5000; + $cookie_hash = ''; + + $handler = $this + ->getMockBuilder( WC_Session_Handler::class ) + ->setMethods( array( 'get_session_cookie' ) ) + ->getMock(); + + wp_set_current_user( $customer->get_id() ); + + $handler->init(); + $handler->set( 'cart', 'fake cart' ); + $handler->save_data(); + + $handler + ->method( 'get_session_cookie' ) + ->willReturn( array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) ); + + wp_set_current_user( 1 ); + + add_filter( 'woocommerce_set_cookie_enabled', '__return_false' ); + + $handler->init_session_cookie(); + + remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' ); + + $this->assertFalse( wp_cache_get( $this->cache_prefix . $customer_id, WC_SESSION_CACHE_GROUP ) ); + $this->assertNull( $this->get_session_from_db( $customer_id ) ); + $this->assertNotNull( $this->get_session_from_db( '1' ) ); + } + /** * Helper function to create a WC session and save it to the DB. */ protected function create_session() { - $this->handler->init(); wp_set_current_user( 1 ); + $this->handler->init(); $this->handler->set( 'cart', 'fake cart' ); $this->handler->save_data(); $this->session_key = $this->handler->get_customer_id();