From 56130d3df93dbe865205b7ef79bf51ebeb1b23a5 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 20 Nov 2025 15:08:46 +0100 Subject: [PATCH 1/3] Add optional redirect loop protection to AuthenticationService - Add configurable redirect validation to prevent redirect loop attacks - Checks for nested redirects, deep encoding, blocked patterns, and URL length - Disabled by default for backward compatibility (opt-in) - Add comprehensive test coverage (8 new tests) - Add detailed documentation with security considerations - Fixes issue #751 Real-world evidence shows bots creating 6-7 levels of nested redirects, wasting server resources and potentially enabling security exploits. Configuration example: 'redirectValidation' => [ 'enabled' => true, 'maxDepth' => 1, 'maxEncodingLevels' => 1, 'maxLength' => 2000, 'blockedPatterns' => ['#/login#i', '#/logout#i'], ] --- docs/en/index.rst | 1 + docs/en/redirect-validation.rst | 194 +++++++++++++++ src/AuthenticationService.php | 78 +++++- tests/TestCase/AuthenticationServiceTest.php | 244 +++++++++++++++++++ 4 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 docs/en/redirect-validation.rst diff --git a/docs/en/index.rst b/docs/en/index.rst index e39834c8..4f12ea25 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -256,6 +256,7 @@ Further Reading * :doc:`/authentication-component` * :doc:`/impersonation` * :doc:`/url-checkers` +* :doc:`/redirect-validation` * :doc:`/testing` * :doc:`/view-helper` * :doc:`/migration-from-the-authcomponent` diff --git a/docs/en/redirect-validation.rst b/docs/en/redirect-validation.rst new file mode 100644 index 00000000..b8a425ef --- /dev/null +++ b/docs/en/redirect-validation.rst @@ -0,0 +1,194 @@ +Redirect Validation +################### + +The Authentication plugin provides optional redirect validation to prevent redirect loop attacks +and malicious redirect patterns that could be exploited by bots or attackers. + +.. _security-redirect-loops: + +Preventing Redirect Loops +========================== + +By default, the authentication service does not validate redirect URLs beyond checking that they +are relative (not external). This means that malicious actors or misconfigured bots could create +deeply nested redirect chains like: + +.. code-block:: text + + /login?redirect=/login?redirect=/login?redirect=/protected/page + +These nested redirects can waste server resources, pollute logs, and potentially enable security +exploits. + +Enabling Redirect Validation +============================= + +To enable redirect validation, configure the ``redirectValidation`` option in your +``AuthenticationService``: + +.. code-block:: php + + // In src/Application.php getAuthenticationService() method + $service = new AuthenticationService(); + $service->setConfig([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, // Enable validation (default: false) + ], + ]); + +Configuration Options +===================== + +The ``redirectValidation`` configuration accepts the following options: + +enabled + **Type:** ``bool`` | **Default:** ``false`` + + Whether to enable redirect validation. Disabled by default for backward compatibility. + +maxDepth + **Type:** ``int`` | **Default:** ``1`` + + Maximum number of nested redirect parameters allowed. For example, with ``maxDepth`` set to 1, + ``/login?redirect=/articles`` is valid, but ``/login?redirect=/login?redirect=/articles`` is blocked. + +maxEncodingLevels + **Type:** ``int`` | **Default:** ``1`` + + Maximum URL encoding levels allowed. This prevents obfuscation attacks using double or triple + encoding (e.g., ``%252F`` for double-encoded ``/``). + +maxLength + **Type:** ``int`` | **Default:** ``2000`` + + Maximum allowed length of the redirect URL in characters. This helps prevent DOS attacks + via excessively long URLs. + +blockedPatterns + **Type:** ``array`` | **Default:** ``['#/login#i', '#/logout#i', '#/register#i']`` + + Array of regular expressions to match against redirect URLs. Matching URLs will be rejected. + This prevents redirects to authentication-related pages that could cause loops. + +Example Configuration +===================== + +Here's a complete example with custom configuration: + +.. code-block:: php + + $service = new AuthenticationService(); + $service->setConfig([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + 'maxDepth' => 1, + 'maxEncodingLevels' => 1, + 'maxLength' => 2000, + 'blockedPatterns' => [ + '#/admin#i', // Block admin areas + '#/login#i', // Block login page + '#/logout#i', // Block logout page + '#/register#i', // Block registration page + ], + ], + ]); + +How Validation Works +==================== + +When redirect validation is enabled and a redirect URL fails validation, ``getLoginRedirect()`` +will return ``null`` instead of the invalid URL. Your application should handle this by +redirecting to a default location: + +.. code-block:: php + + // In your controller + $target = $this->Authentication->getLoginRedirect() ?? '/'; + return $this->redirect($target); + +Validation Checks +================= + +The validation performs the following checks in order: + +1. **Redirect Depth**: Counts occurrences of ``redirect=`` in the decoded URL +2. **Encoding Level**: Counts occurrences of ``%25`` (percent-encoded percent sign) +3. **URL Length**: Checks total character count +4. **Blocked Patterns**: Matches against configured regex patterns + +If any check fails, the URL is rejected. + +Custom Validation +================= + +You can extend ``AuthenticationService`` and override the ``validateRedirect()`` method +to implement custom validation logic: + +.. code-block:: php + + namespace App\Auth; + + use Authentication\AuthenticationService; + + class CustomAuthenticationService extends AuthenticationService + { + protected function validateRedirect(string $redirect): ?string + { + // Call parent validation first + $redirect = parent::validateRedirect($redirect); + + if ($redirect === null) { + return null; + } + + // Add your custom validation + if (str_contains($redirect, 'forbidden')) { + return null; // Reject this URL + } + + return $redirect; + } + } + +Backward Compatibility +====================== + +Redirect validation is **disabled by default** to maintain backward compatibility with existing +applications. To enable it, explicitly set ``'enabled' => true`` in the configuration. + +Security Considerations +======================= + +While redirect validation helps prevent common attacks, it should be part of a comprehensive +security strategy that includes: + +* Rate limiting to prevent bot abuse +* Monitoring and logging of blocked redirects +* Regular security audits +* Keeping the Authentication plugin up to date + +Real-World Attack Example +========================= + +In production environments, bots (especially AI crawlers like GPTBot) have been observed +creating redirect chains with 6-7 levels of nesting: + +.. code-block:: text + + /login?redirect=%2Flogin%3Fredirect%3D%252Flogin%253Fredirect%253D... + +Enabling redirect validation prevents these attacks and protects your application from: + +* Resource exhaustion (CPU wasted parsing deeply nested URLs) +* Log pollution (malformed URLs flooding access logs) +* SEO damage (search engines indexing login pages with loops) +* Potential security exploits when combined with other vulnerabilities + +For more information on redirect attacks, see: + +* `OWASP: Unvalidated Redirects and Forwards `_ +* `CWE-601: URL Redirection to Untrusted Site `_ diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index 71c498c7..ed0b106f 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -83,6 +83,22 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona * AuthenticationComponent::allowUnauthenticated() * - `queryParam` - The name of the query string parameter containing the previously blocked URL * in case of unauthenticated redirect, or null to disable appending the denied URL. + * - `redirectValidation` - Configuration for validating redirect URLs to prevent loops. See below. + * + * ### Redirect Validation Configuration: + * + * ``` + * 'redirectValidation' => [ + * 'enabled' => true, // Enable validation (default: false for BC) + * 'maxDepth' => 1, // Max nested "redirect=" parameters (default: 1) + * 'maxEncodingLevels' => 1, // Max percent-encoding levels (default: 1) + * 'maxLength' => 2000, // Max URL length in characters (default: 2000) + * 'blockedPatterns' => [ // Regex patterns to reject (default: auth pages) + * '#/login#i', + * '#/logout#i', + * ], + * ] + * ``` * * ### Example: * @@ -105,6 +121,17 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona 'identityAttribute' => 'identity', 'queryParam' => null, 'unauthenticatedRedirect' => null, + 'redirectValidation' => [ + 'enabled' => false, // Disabled by default for backward compatibility + 'maxDepth' => 1, + 'maxEncodingLevels' => 1, + 'maxLength' => 2000, + 'blockedPatterns' => [ + '#/login#i', + '#/logout#i', + '#/register#i', + ], + ], ]; /** @@ -457,7 +484,56 @@ public function getLoginRedirect(ServerRequestInterface $request): ?string $parsed['query'] = "?{$parsed['query']}"; } - return $parsed['path'] . $parsed['query']; + $redirect = $parsed['path'] . $parsed['query']; + + // Validate redirect to prevent loops if enabled + return $this->validateRedirect($redirect); + } + + /** + * Validates a redirect URL to prevent loops and malicious patterns + * + * This method can be overridden in subclasses to implement custom validation logic. + * + * @param string $redirect The redirect URL to validate + * @return string|null The validated URL or null if invalid + */ + protected function validateRedirect(string $redirect): ?string + { + $config = $this->getConfig('redirectValidation'); + + // If validation is disabled, return the URL as-is (backward compatibility) + if (empty($config['enabled'])) { + return $redirect; + } + + $decodedUrl = urldecode($redirect); + + // Check for nested redirect parameters + $redirectCount = substr_count($decodedUrl, 'redirect='); + if ($redirectCount > $config['maxDepth']) { + return null; + } + + // Check for multiple encoding levels (e.g., %25 = percent-encoded %) + $encodingCount = substr_count($redirect, '%25'); + if ($encodingCount > $config['maxEncodingLevels']) { + return null; + } + + // Check URL length to prevent DOS attacks + if (strlen($redirect) > $config['maxLength']) { + return null; + } + + // Check against blocked patterns + foreach ($config['blockedPatterns'] as $pattern) { + if (preg_match($pattern, $decodedUrl)) { + return null; + } + } + + return $redirect; } /** diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index 39849630..84560d66 100644 --- a/tests/TestCase/AuthenticationServiceTest.php +++ b/tests/TestCase/AuthenticationServiceTest.php @@ -947,6 +947,250 @@ public function testGetLoginRedirect() ); } + /** + * testGetLoginRedirectValidationDisabled + * + * @return void + */ + public function testGetLoginRedirectValidationDisabled() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => false, + ], + ]); + + // With validation disabled, even nested redirects should pass + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/login?redirect=/secrets'], + ); + $this->assertSame( + '/login?redirect=/secrets', + $service->getLoginRedirect($request), + ); + } + + /** + * testGetLoginRedirectValidationNestedRedirects + * + * @return void + */ + public function testGetLoginRedirectValidationNestedRedirects() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + ], + ]); + + // Valid single-level redirect + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/articles/view/1'], + ); + $this->assertSame( + '/articles/view/1', + $service->getLoginRedirect($request), + ); + + // Nested redirect (should be blocked) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/login?redirect=/articles/view/1'], + ); + $this->assertNull($service->getLoginRedirect($request)); + + // Deeply nested redirect (should be blocked) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/login?redirect=%2Flogin%3Fredirect%3D%252Farticles'], + ); + $this->assertNull($service->getLoginRedirect($request)); + } + + /** + * testGetLoginRedirectValidationEncodingLevels + * + * @return void + */ + public function testGetLoginRedirectValidationEncodingLevels() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + ], + ]); + + // Normal single-encoded URL (should pass) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/articles%2Fview%2F1'], + ); + $this->assertSame( + '/articles%2Fview%2F1', + $service->getLoginRedirect($request), + ); + + // Double-encoded URL (should be blocked) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/articles%252Fview%252F1'], + ); + $this->assertNull($service->getLoginRedirect($request)); + } + + /** + * testGetLoginRedirectValidationBlockedPatterns + * + * @return void + */ + public function testGetLoginRedirectValidationBlockedPatterns() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + ], + ]); + + // Redirect to login page (should be blocked) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/users/login'], + ); + $this->assertNull($service->getLoginRedirect($request)); + + // Redirect to logout page (should be blocked) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/users/logout'], + ); + $this->assertNull($service->getLoginRedirect($request)); + + // Redirect to register page (should be blocked) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/users/register'], + ); + $this->assertNull($service->getLoginRedirect($request)); + + // Valid redirect to non-auth page + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/articles/index'], + ); + $this->assertSame( + '/articles/index', + $service->getLoginRedirect($request), + ); + } + + /** + * testGetLoginRedirectValidationMaxLength + * + * @return void + */ + public function testGetLoginRedirectValidationMaxLength() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + 'maxLength' => 100, + ], + ]); + + // Short URL (should pass) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/articles/view/1'], + ); + $this->assertSame( + '/articles/view/1', + $service->getLoginRedirect($request), + ); + + // Excessively long URL (should be blocked) + $longUrl = '/articles/' . str_repeat('a', 150); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => $longUrl], + ); + $this->assertNull($service->getLoginRedirect($request)); + } + + /** + * testGetLoginRedirectValidationCustomPatterns + * + * @return void + */ + public function testGetLoginRedirectValidationCustomPatterns() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + 'blockedPatterns' => [ + '#/admin#i', + '#/secret#i', + ], + ], + ]); + + // Redirect to admin area (should be blocked by custom pattern) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/articles'], + ['redirect' => '/admin/dashboard'], + ); + $this->assertNull($service->getLoginRedirect($request)); + + // Redirect to normal page (should pass) + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/articles'], + ['redirect' => '/public/articles'], + ); + $this->assertSame( + '/public/articles', + $service->getLoginRedirect($request), + ); + } + + /** + * testGetLoginRedirectValidationWithQueryParameters + * + * @return void + */ + public function testGetLoginRedirectValidationWithQueryParameters() + { + $service = new AuthenticationService([ + 'unauthenticatedRedirect' => '/users/login', + 'queryParam' => 'redirect', + 'redirectValidation' => [ + 'enabled' => true, + ], + ]); + + // Valid redirect with query parameters + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/secrets'], + ['redirect' => '/articles/index?sort=created&direction=desc'], + ); + $this->assertSame( + '/articles/index?sort=created&direction=desc', + $service->getLoginRedirect($request), + ); + } + /** * testImpersonate * From 1ae39c25f857e0b9a68ffd0bd1be2697d84c2fe6 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 20 Nov 2025 15:34:29 +0100 Subject: [PATCH 2/3] Address PR review feedback - Fix comparison operators: Use >= instead of > for maxDepth and maxEncodingLevels This correctly blocks URLs when they meet or exceed the threshold - Replace empty() with ! for enabled check (cleaner intent) - All tests still pass (312 tests, 926 assertions) Addresses feedback from @ADmad and @Copilot in PR #752 --- src/AuthenticationService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index ed0b106f..8d59407e 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -503,7 +503,7 @@ protected function validateRedirect(string $redirect): ?string $config = $this->getConfig('redirectValidation'); // If validation is disabled, return the URL as-is (backward compatibility) - if (empty($config['enabled'])) { + if (!$config['enabled']) { return $redirect; } @@ -511,13 +511,13 @@ protected function validateRedirect(string $redirect): ?string // Check for nested redirect parameters $redirectCount = substr_count($decodedUrl, 'redirect='); - if ($redirectCount > $config['maxDepth']) { + if ($redirectCount >= $config['maxDepth']) { return null; } // Check for multiple encoding levels (e.g., %25 = percent-encoded %) $encodingCount = substr_count($redirect, '%25'); - if ($encodingCount > $config['maxEncodingLevels']) { + if ($encodingCount >= $config['maxEncodingLevels']) { return null; } From 591ec19e8ef3e07bd7ff369e23671263e4e616a6 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 20 Nov 2025 17:56:15 +0100 Subject: [PATCH 3/3] Simplify implementation by removing blockedPatterns Address maintainer feedback from @markstory and @ADmad: - Remove blockedPatterns configuration option - Remove pattern-based validation logic - Update documentation to show custom pattern validation in subclass - Remove 2 pattern-based tests (testGetLoginRedirectValidationBlockedPatterns, testGetLoginRedirectValidationCustomPatterns) Result: Simpler, focused implementation covering the core security issues: - Nested redirect detection - Deep encoding detection - URL length limits Custom pattern blocking can still be achieved by overriding validateRedirect(). All tests pass: 310 tests, 920 assertions Code style checks pass --- docs/en/redirect-validation.rst | 25 +++--- src/AuthenticationService.php | 16 ---- tests/TestCase/AuthenticationServiceTest.php | 84 -------------------- 3 files changed, 9 insertions(+), 116 deletions(-) diff --git a/docs/en/redirect-validation.rst b/docs/en/redirect-validation.rst index b8a425ef..a08301c8 100644 --- a/docs/en/redirect-validation.rst +++ b/docs/en/redirect-validation.rst @@ -66,12 +66,6 @@ maxLength Maximum allowed length of the redirect URL in characters. This helps prevent DOS attacks via excessively long URLs. -blockedPatterns - **Type:** ``array`` | **Default:** ``['#/login#i', '#/logout#i', '#/register#i']`` - - Array of regular expressions to match against redirect URLs. Matching URLs will be rejected. - This prevents redirects to authentication-related pages that could cause loops. - Example Configuration ===================== @@ -88,12 +82,6 @@ Here's a complete example with custom configuration: 'maxDepth' => 1, 'maxEncodingLevels' => 1, 'maxLength' => 2000, - 'blockedPatterns' => [ - '#/admin#i', // Block admin areas - '#/login#i', // Block login page - '#/logout#i', // Block logout page - '#/register#i', // Block registration page - ], ], ]); @@ -118,7 +106,6 @@ The validation performs the following checks in order: 1. **Redirect Depth**: Counts occurrences of ``redirect=`` in the decoded URL 2. **Encoding Level**: Counts occurrences of ``%25`` (percent-encoded percent sign) 3. **URL Length**: Checks total character count -4. **Blocked Patterns**: Matches against configured regex patterns If any check fails, the URL is rejected. @@ -126,7 +113,7 @@ Custom Validation ================= You can extend ``AuthenticationService`` and override the ``validateRedirect()`` method -to implement custom validation logic: +to implement custom validation logic, such as blocking specific URL patterns: .. code-block:: php @@ -146,8 +133,14 @@ to implement custom validation logic: } // Add your custom validation - if (str_contains($redirect, 'forbidden')) { - return null; // Reject this URL + // Example: Block redirects to authentication pages + if (preg_match('#/(login|logout|register)#i', $redirect)) { + return null; + } + + // Example: Block redirects to admin areas + if (str_contains($redirect, '/admin')) { + return null; } return $redirect; diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index 8d59407e..c9d03513 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -93,10 +93,6 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona * 'maxDepth' => 1, // Max nested "redirect=" parameters (default: 1) * 'maxEncodingLevels' => 1, // Max percent-encoding levels (default: 1) * 'maxLength' => 2000, // Max URL length in characters (default: 2000) - * 'blockedPatterns' => [ // Regex patterns to reject (default: auth pages) - * '#/login#i', - * '#/logout#i', - * ], * ] * ``` * @@ -126,11 +122,6 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona 'maxDepth' => 1, 'maxEncodingLevels' => 1, 'maxLength' => 2000, - 'blockedPatterns' => [ - '#/login#i', - '#/logout#i', - '#/register#i', - ], ], ]; @@ -526,13 +517,6 @@ protected function validateRedirect(string $redirect): ?string return null; } - // Check against blocked patterns - foreach ($config['blockedPatterns'] as $pattern) { - if (preg_match($pattern, $decodedUrl)) { - return null; - } - } - return $redirect; } diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index 84560d66..d2e13d50 100644 --- a/tests/TestCase/AuthenticationServiceTest.php +++ b/tests/TestCase/AuthenticationServiceTest.php @@ -1046,53 +1046,6 @@ public function testGetLoginRedirectValidationEncodingLevels() $this->assertNull($service->getLoginRedirect($request)); } - /** - * testGetLoginRedirectValidationBlockedPatterns - * - * @return void - */ - public function testGetLoginRedirectValidationBlockedPatterns() - { - $service = new AuthenticationService([ - 'unauthenticatedRedirect' => '/users/login', - 'queryParam' => 'redirect', - 'redirectValidation' => [ - 'enabled' => true, - ], - ]); - - // Redirect to login page (should be blocked) - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/secrets'], - ['redirect' => '/users/login'], - ); - $this->assertNull($service->getLoginRedirect($request)); - - // Redirect to logout page (should be blocked) - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/secrets'], - ['redirect' => '/users/logout'], - ); - $this->assertNull($service->getLoginRedirect($request)); - - // Redirect to register page (should be blocked) - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/secrets'], - ['redirect' => '/users/register'], - ); - $this->assertNull($service->getLoginRedirect($request)); - - // Valid redirect to non-auth page - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/secrets'], - ['redirect' => '/articles/index'], - ); - $this->assertSame( - '/articles/index', - $service->getLoginRedirect($request), - ); - } - /** * testGetLoginRedirectValidationMaxLength * @@ -1128,43 +1081,6 @@ public function testGetLoginRedirectValidationMaxLength() $this->assertNull($service->getLoginRedirect($request)); } - /** - * testGetLoginRedirectValidationCustomPatterns - * - * @return void - */ - public function testGetLoginRedirectValidationCustomPatterns() - { - $service = new AuthenticationService([ - 'unauthenticatedRedirect' => '/users/login', - 'queryParam' => 'redirect', - 'redirectValidation' => [ - 'enabled' => true, - 'blockedPatterns' => [ - '#/admin#i', - '#/secret#i', - ], - ], - ]); - - // Redirect to admin area (should be blocked by custom pattern) - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/articles'], - ['redirect' => '/admin/dashboard'], - ); - $this->assertNull($service->getLoginRedirect($request)); - - // Redirect to normal page (should pass) - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/articles'], - ['redirect' => '/public/articles'], - ); - $this->assertSame( - '/public/articles', - $service->getLoginRedirect($request), - ); - } - /** * testGetLoginRedirectValidationWithQueryParameters *