Skip to content

Commit 2ba7ed9

Browse files
committed
Merge branch '3.x' into 4.x
- Use lazy identifier initialization in getIdentifier() (3.x approach) - Add AuthenticationPlugin as main plugin class, Plugin as deprecated alias - Add redirect validation feature from 3.x - Update all authenticators to use getIdentifier() instead of direct property access
2 parents 4325675 + 8575216 commit 2ba7ed9

23 files changed

Lines changed: 659 additions & 111 deletions

.github/workflows/deploy_docs_2x.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Cloning repo
15-
uses: actions/checkout@v5
15+
uses: actions/checkout@v6
1616
with:
1717
fetch-depth: 0
1818

.github/workflows/deploy_docs_3x.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Cloning repo
15-
uses: actions/checkout@v5
15+
uses: actions/checkout@v6
1616
with:
1717
fetch-depth: 0
1818

docs/en/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ Further Reading
256256
* :doc:`/authentication-component`
257257
* :doc:`/impersonation`
258258
* :doc:`/url-checkers`
259+
* :doc:`/redirect-validation`
259260
* :doc:`/testing`
260261
* :doc:`/view-helper`
261262
* :doc:`/migration-from-the-authcomponent`

docs/en/migration-from-the-authcomponent.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ this exception into a redirect using the ``unauthenticatedRedirect``
263263
when configuring the ``AuthenticationService``.
264264

265265
You can also pass the current request target URI as a query parameter
266-
using the ``queryParam`` option::
266+
using the ``queryParam`` option. Note that the redirect parameter is only
267+
appended for GET requests to prevent redirecting to non-GET actions after login::
267268

268269
// In the getAuthenticationService() method of your src/Application.php
269270

@@ -308,8 +309,9 @@ leveraging the ``AuthenticationService``::
308309
if ($result->isValid()) {
309310
$authService = $this->Authentication->getAuthenticationService();
310311

311-
// Assuming you are using the `Password` identifier.
312-
if ($authService->identifiers()->get('Password')->needsPasswordRehash()) {
312+
// Get the identifier that was used for authentication.
313+
$identifier = $authService->getIdentificationProvider();
314+
if ($identifier !== null && $identifier->needsPasswordRehash()) {
313315
// Rehash happens on save.
314316
$user = $this->Users->get($this->Authentication->getIdentityData('id'));
315317
$user->password = $this->request->getData('password');

docs/en/redirect-validation.rst

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
Redirect Validation
2+
###################
3+
4+
The Authentication plugin provides optional redirect validation to prevent redirect loop attacks
5+
and malicious redirect patterns that could be exploited by bots or attackers.
6+
7+
.. _security-redirect-loops:
8+
9+
Preventing Redirect Loops
10+
==========================
11+
12+
By default, the authentication service does not validate redirect URLs beyond checking that they
13+
are relative (not external). This means that malicious actors or misconfigured bots could create
14+
deeply nested redirect chains like:
15+
16+
.. code-block:: text
17+
18+
/login?redirect=/login?redirect=/login?redirect=/protected/page
19+
20+
These nested redirects can waste server resources, pollute logs, and potentially enable security
21+
exploits.
22+
23+
Enabling Redirect Validation
24+
=============================
25+
26+
To enable redirect validation, configure the ``redirectValidation`` option in your
27+
``AuthenticationService``:
28+
29+
.. code-block:: php
30+
31+
// In src/Application.php getAuthenticationService() method
32+
$service = new AuthenticationService();
33+
$service->setConfig([
34+
'unauthenticatedRedirect' => '/users/login',
35+
'queryParam' => 'redirect',
36+
'redirectValidation' => [
37+
'enabled' => true, // Enable validation (default: false)
38+
],
39+
]);
40+
41+
Configuration Options
42+
=====================
43+
44+
The ``redirectValidation`` configuration accepts the following options:
45+
46+
enabled
47+
**Type:** ``bool`` | **Default:** ``false``
48+
49+
Whether to enable redirect validation. Disabled by default for backward compatibility.
50+
51+
maxDepth
52+
**Type:** ``int`` | **Default:** ``1``
53+
54+
Maximum number of nested redirect parameters allowed. For example, with ``maxDepth`` set to 1,
55+
``/login?redirect=/articles`` is valid, but ``/login?redirect=/login?redirect=/articles`` is blocked.
56+
57+
maxEncodingLevels
58+
**Type:** ``int`` | **Default:** ``1``
59+
60+
Maximum URL encoding levels allowed. This prevents obfuscation attacks using double or triple
61+
encoding (e.g., ``%252F`` for double-encoded ``/``).
62+
63+
maxLength
64+
**Type:** ``int`` | **Default:** ``2000``
65+
66+
Maximum allowed length of the redirect URL in characters. This helps prevent DOS attacks
67+
via excessively long URLs.
68+
69+
Example Configuration
70+
=====================
71+
72+
Here's a complete example with custom configuration:
73+
74+
.. code-block:: php
75+
76+
$service = new AuthenticationService();
77+
$service->setConfig([
78+
'unauthenticatedRedirect' => '/users/login',
79+
'queryParam' => 'redirect',
80+
'redirectValidation' => [
81+
'enabled' => true,
82+
'maxDepth' => 1,
83+
'maxEncodingLevels' => 1,
84+
'maxLength' => 2000,
85+
],
86+
]);
87+
88+
How Validation Works
89+
====================
90+
91+
When redirect validation is enabled and a redirect URL fails validation, ``getLoginRedirect()``
92+
will return ``null`` instead of the invalid URL. Your application should handle this by
93+
redirecting to a default location:
94+
95+
.. code-block:: php
96+
97+
// In your controller
98+
$target = $this->Authentication->getLoginRedirect() ?? '/';
99+
return $this->redirect($target);
100+
101+
Validation Checks
102+
=================
103+
104+
The validation performs the following checks in order:
105+
106+
1. **Redirect Depth**: Counts occurrences of ``redirect=`` in the decoded URL
107+
2. **Encoding Level**: Counts occurrences of ``%25`` (percent-encoded percent sign)
108+
3. **URL Length**: Checks total character count
109+
110+
If any check fails, the URL is rejected.
111+
112+
Custom Validation
113+
=================
114+
115+
You can extend ``AuthenticationService`` and override the ``validateRedirect()`` method
116+
to implement custom validation logic, such as blocking specific URL patterns:
117+
118+
.. code-block:: php
119+
120+
namespace App\Auth;
121+
122+
use Authentication\AuthenticationService;
123+
124+
class CustomAuthenticationService extends AuthenticationService
125+
{
126+
protected function validateRedirect(string $redirect): ?string
127+
{
128+
// Call parent validation first
129+
$redirect = parent::validateRedirect($redirect);
130+
131+
if ($redirect === null) {
132+
return null;
133+
}
134+
135+
// Add your custom validation
136+
// Example: Block redirects to authentication pages
137+
if (preg_match('#/(login|logout|register)#i', $redirect)) {
138+
return null;
139+
}
140+
141+
// Example: Block redirects to admin areas
142+
if (str_contains($redirect, '/admin')) {
143+
return null;
144+
}
145+
146+
return $redirect;
147+
}
148+
}
149+
150+
Backward Compatibility
151+
======================
152+
153+
Redirect validation is **disabled by default** to maintain backward compatibility with existing
154+
applications. To enable it, explicitly set ``'enabled' => true`` in the configuration.
155+
156+
Security Considerations
157+
=======================
158+
159+
While redirect validation helps prevent common attacks, it should be part of a comprehensive
160+
security strategy that includes:
161+
162+
* Rate limiting to prevent bot abuse
163+
* Monitoring and logging of blocked redirects
164+
* Regular security audits
165+
* Keeping the Authentication plugin up to date
166+
167+
Real-World Attack Example
168+
=========================
169+
170+
In production environments, bots (especially AI crawlers like GPTBot) have been observed
171+
creating redirect chains with 6-7 levels of nesting:
172+
173+
.. code-block:: text
174+
175+
/login?redirect=%2Flogin%3Fredirect%3D%252Flogin%253Fredirect%253D...
176+
177+
Enabling redirect validation prevents these attacks and protects your application from:
178+
179+
* Resource exhaustion (CPU wasted parsing deeply nested URLs)
180+
* Log pollution (malformed URLs flooding access logs)
181+
* SEO damage (search engines indexing login pages with loops)
182+
* Potential security exploits when combined with other vulnerabilities
183+
184+
For more information on redirect attacks, see:
185+
186+
* `OWASP: Unvalidated Redirects and Forwards <https://owasp.org/www-community/attacks/Unvalidated_Redirects_and_Forwards>`_
187+
* `CWE-601: URL Redirection to Untrusted Site <https://cwe.mitre.org/data/definitions/601.html>`_

docs/fr/migration-from-the-authcomponent.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,9 @@ pouvez convertir cette exception en redirection en utilisant
257257
l'\ ``AuthenticationService``.
258258

259259
Vous pouvez aussi passer l'URI ciblée par la requête en cours en tant que
260-
paramètre dans la query string de la redirection avec l'option ``queryParam``::
260+
paramètre dans la query string de la redirection avec l'option ``queryParam``.
261+
Notez que le paramètre de redirection n'est ajouté que pour les requêtes GET afin
262+
d'éviter de rediriger vers des actions non-GET après la connexion::
261263

262264
// Dans la méthode getAuthenticationService() de votre src/Application.php
263265

@@ -303,8 +305,9 @@ parti de l'\ ``AuthenticationService``::
303305
if ($result->isValid()) {
304306
$authService = $this->Authentication->getAuthenticationService();
305307

306-
// En supposant que vous utilisez l'identificateur `Password`.
307-
if ($authService->identifiers()->get('Password')->needsPasswordRehash()) {
308+
// Obtenir l'identificateur qui a été utilisé pour l'authentification.
309+
$identifier = $authService->getIdentificationProvider();
310+
if ($identifier !== null && $identifier->needsPasswordRehash()) {
308311
// Le re-hachage se produit lors de la sauvegarde.
309312
$user = $this->Users->get($this->Authentication->getIdentityData('id'));
310313
$user->password = $this->request->getData('password');

docs/ja/migration-from-the-authcomponent.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ this exception into a redirect using the ``unauthenticatedRedirect``
229229
when configuring the ``AuthenticationService``.
230230

231231
You can also pass the current request target URI as a query parameter
232-
using the ``queryParam`` option::
232+
using the ``queryParam`` option. Note that the redirect parameter is only
233+
appended for GET requests to prevent redirecting to non-GET actions after login::
233234

234235
// In the getAuthenticationService() method of your src/Application.php
235236

@@ -273,8 +274,9 @@ using the ``queryParam`` option::
273274
if ($result->isValid()) {
274275
$authService = $this->Authentication->getAuthenticationService();
275276

276-
// 識別子に `Password` を使用していると仮定します。
277-
if ($authService->identifiers()->get('Password')->needsPasswordRehash()) {
277+
// 認証に使用された識別子を取得します。
278+
$identifier = $authService->getIdentificationProvider();
279+
if ($identifier !== null && $identifier->needsPasswordRehash()) {
278280
// セーブ時にリハッシュが発生します。
279281
$user = $this->Users->get($this->Authentication->getIdentityData('id'));
280282
$user->password = $this->request->getData('password');

src/AuthenticationPlugin.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7+
*
8+
* Licensed under The MIT License
9+
* Redistributions of files must retain the above copyright notice.
10+
*
11+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
12+
* @link https://cakephp.org CakePHP(tm) Project
13+
* @since 1.0.2
14+
* @license https://www.opensource.org/licenses/mit-license.php MIT License
15+
*/
16+
namespace Authentication;
17+
18+
use Cake\Core\BasePlugin;
19+
20+
/**
21+
* Plugin class for CakePHP.
22+
*/
23+
class AuthenticationPlugin extends BasePlugin
24+
{
25+
/**
26+
* Do bootstrapping or not
27+
*
28+
* @var bool
29+
*/
30+
protected bool $bootstrapEnabled = false;
31+
32+
/**
33+
* Load routes or not
34+
*
35+
* @var bool
36+
*/
37+
protected bool $routesEnabled = false;
38+
39+
/**
40+
* Console middleware
41+
*
42+
* @var bool
43+
*/
44+
protected bool $consoleEnabled = false;
45+
}

0 commit comments

Comments
 (0)