From ea57edf115734565814ef09091e3b10ea5c51771 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 27 May 2026 19:13:47 -0600 Subject: [PATCH] wip: scope SSO loop counter --- inc/sso/class-sso.php | 30 ++++++++++++++++++++++++++- tests/WP_Ultimo/SSO/SSO_Test.php | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/inc/sso/class-sso.php b/inc/sso/class-sso.php index cb0c1dbda..63ec842ce 100644 --- a/inc/sso/class-sso.php +++ b/inc/sso/class-sso.php @@ -438,7 +438,7 @@ private function should_skip_redirect_due_to_loop(): bool { $threshold = (int) apply_filters('wu_sso_redirect_loop_threshold', 3); $window = (int) apply_filters('wu_sso_redirect_loop_window', 120); - if ($threshold < 1 || $window < 1) { + if ($threshold < 1 || $window < 1 || ! $this->is_sso_loop_request()) { return false; } @@ -466,6 +466,34 @@ private function should_skip_redirect_due_to_loop(): bool { return $count >= $threshold; } + /** + * Check if the current request looks like an SSO loop hop. + * + * Plain logged-out visits to protected URLs can call auth_redirect() + * repeatedly without ever entering the SSO flow. Only requests carrying an + * SSO fingerprint should consume the redirect-loop budget. + * + * @since 2.0.11 + * @return bool True when the request carries an SSO fingerprint. + */ + private function is_sso_loop_request(): bool { + + if ($this->input('wu_sso_token') || 'login' === $this->input('sso') || $this->input('return_url') || $this->input('_jsonp')) { + return true; + } + + $referer = wp_get_referer(); + + if ( ! $referer) { + return false; + } + + $referer_path = (string) wp_parse_url($referer, PHP_URL_PATH); + $sso_path = '/' . ltrim($this->get_url_path(), '/'); + + return 0 === strpos($referer_path, $sso_path); + } + /** * Listens for SSO requests and route them to the correct handler. * diff --git a/tests/WP_Ultimo/SSO/SSO_Test.php b/tests/WP_Ultimo/SSO/SSO_Test.php index af3ec9b0b..5255aeb49 100644 --- a/tests/WP_Ultimo/SSO/SSO_Test.php +++ b/tests/WP_Ultimo/SSO/SSO_Test.php @@ -45,10 +45,13 @@ protected function tearDown(): void { unset($_REQUEST['broker']); unset($_REQUEST['sso_verify']); unset($_REQUEST['wu_sso_token']); + unset($_REQUEST['sso']); unset($_REQUEST['return_url']); unset($_REQUEST['redirect_to']); + unset($_REQUEST['_jsonp']); unset($_COOKIE['wu_sso_denied']); unset($_COOKIE['wu_sso_redirect_attempts']); + unset($_SERVER['HTTP_REFERER']); remove_all_filters('wu_sso_redirect_loop_threshold'); remove_all_filters('wu_sso_redirect_loop_window'); @@ -253,6 +256,8 @@ public function test_get_sso_redirect_to_defaults_cross_domain_return_url_to_adm public function test_sso_redirect_loop_counter_skips_at_threshold(): void { $sso = SSO::get_instance(); + $_REQUEST['sso'] = 'login'; + add_filter( 'wu_sso_redirect_loop_threshold', function () { @@ -276,6 +281,8 @@ function () { public function test_sso_redirect_loop_counter_resets_after_window(): void { $sso = SSO::get_instance(); + $_REQUEST['sso'] = 'login'; + add_filter( 'wu_sso_redirect_loop_threshold', function () { @@ -299,6 +306,34 @@ function () { $this->assertSame(1, $this->get_sso_redirect_attempt_count()); } + /** + * Test plain protected URL visits do not consume the SSO loop budget. + */ + public function test_sso_redirect_loop_counter_ignores_non_sso_requests(): void { + $sso = SSO::get_instance(); + + $method = new \ReflectionMethod($sso, 'should_skip_redirect_due_to_loop'); + $method->setAccessible(true); + + $this->assertFalse($method->invoke($sso)); + $this->assertSame(0, $this->get_sso_redirect_attempt_count()); + } + + /** + * Test SSO referers consume the redirect-loop budget. + */ + public function test_sso_redirect_loop_counter_counts_sso_referer(): void { + $sso = SSO::get_instance(); + + $_SERVER['HTTP_REFERER'] = home_url('/sso'); + + $method = new \ReflectionMethod($sso, 'should_skip_redirect_due_to_loop'); + $method->setAccessible(true); + + $this->assertFalse($method->invoke($sso)); + $this->assertSame(1, $this->get_sso_redirect_attempt_count()); + } + /** * Get the redirect-attempt count from the test cookie. *