Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/en/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ Both redirect handlers share the same configuration options:
* ``queryParam`` - the accessed request URL will be attached to the redirect URL
query parameter (``redirect`` by default).
* ``statusCode`` - HTTP status code of a redirect, ``302`` by default.
* ``allowedRedirectExtensions`` - an array of allowed file extensions for redirecting.
If the request URL has a file extension that is not in this list, the redirect will not
happen and the exception will be rethrown. Can also be a boolean to toggle on/off
redirects entirely. This is useful to prevent unauthorized access to API based
responses, that should not be redirecting in any case. `true` by default and not enabled then.

For example::

Expand All @@ -212,6 +217,7 @@ For example::
MissingIdentityException::class,
OtherException::class,
],
'allowedRedirectExtensions' => ['csv', 'pdf'],
],
]));

Expand Down
1 change: 1 addition & 0 deletions src/Middleware/UnauthorizedHandler/CakeRedirectHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class CakeRedirectHandler extends RedirectHandler
],
'queryParam' => 'redirect',
'statusCode' => 302,
'allowedRedirectExtensions' => true,
];

/**
Expand Down
31 changes: 29 additions & 2 deletions src/Middleware/UnauthorizedHandler/RedirectHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class RedirectHandler implements HandlerInterface
* - `url` - Url to redirect to.
* - `queryParam` - Query parameter name for the target url.
* - `statusCode` - Redirection status code.
* - `allowedRedirectExtensions` - If true, redirects are allowed for all extensions.
* Pass specific ones to allow list, or false to disallow redirects for any extension.
*
* @var array
*/
Expand All @@ -44,6 +46,7 @@ class RedirectHandler implements HandlerInterface
'url' => '/login',
'queryParam' => 'redirect',
'statusCode' => 302,
'allowedRedirectExtensions' => true,
];

/**
Expand All @@ -58,7 +61,7 @@ public function handle(
): ResponseInterface {
$options += $this->defaultOptions;

if (!$this->checkException($exception, $options['exceptions'])) {
if (!$this->redirectAllowed($request, $options) || !$this->checkException($exception, $options['exceptions'])) {
throw $exception;
}

Expand Down Expand Up @@ -106,7 +109,7 @@ protected function getUrl(ServerRequestInterface $request, array $options): stri
$redirect .= '?' . $uri->getQuery();
}
$query = urlencode($options['queryParam']) . '=' . urlencode($redirect);
if (strpos($url, '?') !== false) {
if (str_contains($url, '?')) {
$query = '&' . $query;
} else {
$query = '?' . $query;
Expand All @@ -117,4 +120,28 @@ protected function getUrl(ServerRequestInterface $request, array $options): stri

return $url;
}

/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param array $options
* @return bool
*/
protected function redirectAllowed(ServerRequestInterface $request, array $options): bool
{
$extensions = $options['allowedRedirectExtensions'] ?? true;
if ($extensions === false) {
return false;
}
if ($extensions === true) {
return true;
}

/** @var \Cake\Http\ServerRequest $request */
$currentExtension = $request->getParam('_ext');
if (!$currentExtension) {
return true;
}

return in_array($currentExtension, (array)$extensions, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
namespace Authorization\Test\TestCase\Middleware\UnauthorizedHandler;

use Authorization\Exception\Exception;
use Authorization\Exception\MissingIdentityException;
use Authorization\Middleware\UnauthorizedHandler\RedirectHandler;
use Cake\Core\Configure;
use Cake\Http\ServerRequestFactory;
use Cake\TestSuite\TestCase;
use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;

class RedirectHandlerTest extends TestCase
Expand Down Expand Up @@ -160,4 +162,91 @@ public function testHandleException()
$this->expectException(Exception::class);
$handler->handle($exception, $request);
}

public function testHandleRedirectionWithExtensionsFalse(): void
{
$handler = new RedirectHandler();

$exception = new MissingIdentityException();
$request = ServerRequestFactory::fromGlobals(
['REQUEST_METHOD' => 'GET'],
);
$request = $request->withParam('_ext', 'csv');

$this->expectException(MissingIdentityException::class);

$handler->handle($exception, $request, [
'exceptions' => [
LogicException::class,
],
'url' => '/users/login',
'allowedRedirectExtensions' => false,
]);
}

public function testHandleRedirectionWithExtension(): void
{
$handler = new RedirectHandler();

$exception = new MissingIdentityException();
$request = ServerRequestFactory::fromGlobals(
['REQUEST_METHOD' => 'GET'],
);
$request = $request->withParam('_ext', 'csv');

$this->expectException(MissingIdentityException::class);

$handler->handle($exception, $request, [
'exceptions' => [
MissingIdentityException::class,
],
'url' => '/users/login',
'allowedRedirectExtensions' => [],
]);
}

public function testHandleRedirectionWithExtensionAllowlisted(): void
{
$handler = new RedirectHandler();

$exception = new MissingIdentityException();
$request = ServerRequestFactory::fromGlobals(
['REQUEST_METHOD' => 'GET'],
);
$request = $request->withParam('_ext', 'csv');

$response = $handler->handle($exception, $request, [
'exceptions' => [
Exception::class,
],
'url' => '/users/login',
'queryParam' => null,
'allowedRedirectExtensions' => ['csv'],
]);

$this->assertSame(302, $response->getStatusCode());
$this->assertSame('/users/login', $response->getHeaderLine('Location'));
}

public function testHandleRedirectionWithExtensionAllowedNoExtensionInRequest(): void
{
$handler = new RedirectHandler();

$exception = new Exception();
$request = ServerRequestFactory::fromGlobals(
['REQUEST_METHOD' => 'GET'],
);

$response = $handler->handle($exception, $request, [
'exceptions' => [
Exception::class,
],
'url' => '/users/login',
'queryParam' => null,
'allowedRedirectExtensions' => ['csv'],
]);

$this->assertSame(302, $response->getStatusCode());
$this->assertSame('/users/login', $response->getHeaderLine('Location'));
}
}