Extract Application methods to their respective middleware

Signed-off-by: Maurício Meneghini Fauth <mauricio@fauth.dev>
This commit is contained in:
Maurício Meneghini Fauth 2023-08-17 11:05:18 -03:00
parent e0dae7b966
commit 2dd24efbe6
No known key found for this signature in database
GPG Key ID: 6A16FD38AFC89CC8
8 changed files with 250 additions and 223 deletions

View File

@ -13,8 +13,6 @@ use PhpMyAdmin\Http\Handler\ApplicationHandler;
use PhpMyAdmin\Http\Handler\QueueRequestHandler;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Identifiers\DatabaseName;
use PhpMyAdmin\Identifiers\TableName;
use PhpMyAdmin\Middleware\Authentication;
use PhpMyAdmin\Middleware\ConfigErrorAndPermissionChecking;
use PhpMyAdmin\Middleware\ConfigLoading;
@ -53,20 +51,9 @@ use PhpMyAdmin\Middleware\ZeroConfPostConnection;
use PhpMyAdmin\Routing\Routing;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Throwable;
use function __;
use function function_exists;
use function hash_equals;
use function is_array;
use function is_scalar;
use function session_id;
use function sprintf;
use function strlen;
use function trigger_error;
use const E_USER_ERROR;
class Application
{
@ -93,7 +80,7 @@ class Application
$requestHandler = new QueueRequestHandler(new ApplicationHandler($this));
$requestHandler->add(new ErrorHandling($this->errorHandler));
$requestHandler->add(new OutputBuffering());
$requestHandler->add(new PhpExtensionsChecking($this, $this->template, $this->responseFactory));
$requestHandler->add(new PhpExtensionsChecking($this->template, $this->responseFactory));
$requestHandler->add(new ServerConfigurationChecking($this->template, $this->responseFactory));
$requestHandler->add(new PhpSettingsConfiguration());
$requestHandler->add(new RouteParsing());
@ -107,8 +94,8 @@ class Application
));
$requestHandler->add(new EncryptedQueryParamsHandling());
$requestHandler->add(new UrlParamsSetting($this->config));
$requestHandler->add(new TokenRequestParamChecking($this));
$requestHandler->add(new DatabaseAndTableSetting($this));
$requestHandler->add(new TokenRequestParamChecking());
$requestHandler->add(new DatabaseAndTableSetting());
$requestHandler->add(new SqlQueryGlobalSetting());
$requestHandler->add(new LanguageLoading());
$requestHandler->add(new ConfigErrorAndPermissionChecking(
@ -162,119 +149,4 @@ class Application
$this->responseFactory,
);
}
/**
* Checks that required PHP extensions are there.
*/
public function checkRequiredPhpExtensions(): void
{
/**
* Warning about mbstring.
*/
if (! function_exists('mb_detect_encoding')) {
Core::warnMissingExtension('mbstring');
}
/**
* We really need this one!
*/
if (! function_exists('preg_replace')) {
Core::warnMissingExtension('pcre', true);
}
/**
* JSON is required in several places.
*/
if (! function_exists('json_encode')) {
Core::warnMissingExtension('json', true);
}
/**
* ctype is required for Twig.
*/
if (! function_exists('ctype_alpha')) {
Core::warnMissingExtension('ctype', true);
}
if (! function_exists('mysqli_connect')) {
$moreInfo = sprintf(__('See %sour documentation%s for more information.'), '[doc@faqmysql]', '[/doc]');
Core::warnMissingExtension('mysqli', true, $moreInfo);
}
if (! function_exists('session_name')) {
Core::warnMissingExtension('session', true);
}
/**
* hash is required for cookie authentication.
*/
if (function_exists('hash_hmac')) {
return;
}
Core::warnMissingExtension('hash', true);
}
/**
* Check whether user supplied token is valid, if not remove any possibly
* dangerous stuff from request.
*
* Check for token mismatch only if the Request method is POST.
* GET Requests would never have token and therefore checking
* mis-match does not make sense.
*/
public function checkTokenRequestParam(): void
{
$GLOBALS['token_mismatch'] = true;
$GLOBALS['token_provided'] = false;
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
return;
}
if (isset($_POST['token']) && is_scalar($_POST['token']) && strlen((string) $_POST['token']) > 0) {
$GLOBALS['token_provided'] = true;
$GLOBALS['token_mismatch'] = ! @hash_equals($_SESSION[' PMA_token '], (string) $_POST['token']);
}
if (! $GLOBALS['token_mismatch']) {
return;
}
// Warn in case the mismatch is result of failed setting of session cookie
if (isset($_POST['set_session']) && $_POST['set_session'] !== session_id()) {
trigger_error(
__(
'Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.',
),
E_USER_ERROR,
);
}
/**
* We don't allow any POST operation parameters if the token is mismatched
* or is not provided.
*/
$allowList = ['ajax_request'];
Sanitize::removeRequestVars($allowList);
}
public function setDatabaseAndTableFromRequest(ContainerInterface $container, ServerRequest $request): void
{
$GLOBALS['urlParams'] ??= null;
$db = DatabaseName::tryFrom($request->getParam('db'));
$table = TableName::tryFrom($request->getParam('table'));
$GLOBALS['db'] = $db?->getName() ?? '';
$GLOBALS['table'] = $table?->getName() ?? '';
if (! is_array($GLOBALS['urlParams'])) {
$GLOBALS['urlParams'] = [];
}
$GLOBALS['urlParams']['db'] = $GLOBALS['db'];
$GLOBALS['urlParams']['table'] = $GLOBALS['table'];
$container->setParameter('url_params', $GLOBALS['urlParams']);
}
}

View File

@ -4,28 +4,46 @@ declare(strict_types=1);
namespace PhpMyAdmin\Middleware;
use PhpMyAdmin\Application;
use PhpMyAdmin\Core;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Identifiers\DatabaseName;
use PhpMyAdmin\Identifiers\TableName;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use function assert;
use function is_array;
final class DatabaseAndTableSetting implements MiddlewareInterface
{
public function __construct(private readonly Application $application)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
assert($request instanceof ServerRequest);
$container = Core::getContainerBuilder();
$this->application->setDatabaseAndTableFromRequest($container, $request);
$this->setDatabaseAndTableFromRequest($container, $request);
return $handler->handle($request);
}
private function setDatabaseAndTableFromRequest(ContainerInterface $container, ServerRequest $request): void
{
$GLOBALS['urlParams'] ??= null;
$db = DatabaseName::tryFrom($request->getParam('db'));
$table = TableName::tryFrom($request->getParam('table'));
$GLOBALS['db'] = $db?->getName() ?? '';
$GLOBALS['table'] = $table?->getName() ?? '';
if (! is_array($GLOBALS['urlParams'])) {
$GLOBALS['urlParams'] = [];
}
$GLOBALS['urlParams']['db'] = $GLOBALS['db'];
$GLOBALS['urlParams']['table'] = $GLOBALS['table'];
$container->setParameter('url_params', $GLOBALS['urlParams']);
}
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace PhpMyAdmin\Middleware;
use Fig\Http\Message\StatusCodeInterface;
use PhpMyAdmin\Application;
use PhpMyAdmin\Core;
use PhpMyAdmin\Exceptions\MissingExtensionException;
use PhpMyAdmin\Http\Factory\ResponseFactory;
use PhpMyAdmin\Template;
@ -14,19 +14,20 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function __;
use function function_exists;
use function sprintf;
final class PhpExtensionsChecking implements MiddlewareInterface
{
public function __construct(
private readonly Application $application,
private readonly Template $template,
private readonly ResponseFactory $responseFactory,
) {
public function __construct(private readonly Template $template, private readonly ResponseFactory $responseFactory)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$this->application->checkRequiredPhpExtensions();
$this->checkRequiredPhpExtensions();
} catch (MissingExtensionException $exception) {
// Disables template caching because the cache directory is not known yet.
$this->template->disableCache();
@ -43,4 +44,56 @@ final class PhpExtensionsChecking implements MiddlewareInterface
return $handler->handle($request);
}
/**
* Checks that required PHP extensions are there.
*/
private function checkRequiredPhpExtensions(): void
{
/**
* Warning about mbstring.
*/
if (! function_exists('mb_detect_encoding')) {
Core::warnMissingExtension('mbstring');
}
/**
* We really need this one!
*/
if (! function_exists('preg_replace')) {
Core::warnMissingExtension('pcre', true);
}
/**
* JSON is required in several places.
*/
if (! function_exists('json_encode')) {
Core::warnMissingExtension('json', true);
}
/**
* ctype is required for Twig.
*/
if (! function_exists('ctype_alpha')) {
Core::warnMissingExtension('ctype', true);
}
if (! function_exists('mysqli_connect')) {
$moreInfo = sprintf(__('See %sour documentation%s for more information.'), '[doc@faqmysql]', '[/doc]');
Core::warnMissingExtension('mysqli', true, $moreInfo);
}
if (! function_exists('session_name')) {
Core::warnMissingExtension('session', true);
}
/**
* hash is required for cookie authentication.
*/
if (function_exists('hash_hmac')) {
return;
}
Core::warnMissingExtension('hash', true);
}
}

View File

@ -4,22 +4,71 @@ declare(strict_types=1);
namespace PhpMyAdmin\Middleware;
use PhpMyAdmin\Application;
use PhpMyAdmin\Sanitize;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function __;
use function hash_equals;
use function is_scalar;
use function session_id;
use function strlen;
use function trigger_error;
use const E_USER_ERROR;
/**
* Check whether user supplied token is valid, if not remove any possibly
* dangerous stuff from request.
*
* Check for token mismatch only if the Request method is POST.
* GET Requests would never have token and therefore checking
* mismatch does not make sense.
*/
final class TokenRequestParamChecking implements MiddlewareInterface
{
public function __construct(private readonly Application $application)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->application->checkTokenRequestParam();
$this->checkTokenRequestParam();
return $handler->handle($request);
}
public function checkTokenRequestParam(): void
{
$GLOBALS['token_mismatch'] = true;
$GLOBALS['token_provided'] = false;
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
return;
}
if (isset($_POST['token']) && is_scalar($_POST['token']) && strlen((string) $_POST['token']) > 0) {
$GLOBALS['token_provided'] = true;
$GLOBALS['token_mismatch'] = ! @hash_equals($_SESSION[' PMA_token '], (string) $_POST['token']);
}
if (! $GLOBALS['token_mismatch']) {
return;
}
// Warn in case the mismatch is result of failed setting of session cookie
if (isset($_POST['set_session']) && $_POST['set_session'] !== session_id()) {
trigger_error(
__(
'Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.',
),
E_USER_ERROR,
);
}
/**
* We don't allow any POST operation parameters if the token is mismatched
* or is not provided.
*/
$allowList = ['ajax_request'];
Sanitize::removeRequestVars($allowList);
}
}

View File

@ -45,21 +45,6 @@ parameters:
count: 1
path: libraries/classes/Advisory/Advisor.php
-
message: "#^Cannot access offset 'table' on mixed\\.$#"
count: 1
path: libraries/classes/Application.php
-
message: "#^Parameter \\#1 \\$known_string of function hash_equals expects string, mixed given\\.$#"
count: 1
path: libraries/classes/Application.php
-
message: "#^Parameter \\#2 \\$value of method Symfony\\\\Component\\\\DependencyInjection\\\\ContainerInterface\\:\\:setParameter\\(\\) expects array\\|bool\\|float\\|int\\|string\\|UnitEnum\\|null, mixed given\\.$#"
count: 1
path: libraries/classes/Application.php
-
message: "#^Cannot access offset 'AllowSharedBookmarks' on mixed\\.$#"
count: 3
@ -15810,6 +15795,16 @@ parameters:
count: 1
path: libraries/classes/Middleware/CurrentServerGlobalSetting.php
-
message: "#^Cannot access offset 'table' on mixed\\.$#"
count: 1
path: libraries/classes/Middleware/DatabaseAndTableSetting.php
-
message: "#^Parameter \\#2 \\$value of method Symfony\\\\Component\\\\DependencyInjection\\\\ContainerInterface\\:\\:setParameter\\(\\) expects array\\|bool\\|float\\|int\\|string\\|UnitEnum\\|null, mixed given\\.$#"
count: 1
path: libraries/classes/Middleware/DatabaseAndTableSetting.php
-
message: "#^Cannot call method getVersion\\(\\) on mixed\\.$#"
count: 1
@ -15835,6 +15830,11 @@ parameters:
count: 1
path: libraries/classes/Middleware/SetupPageRedirection.php
-
message: "#^Parameter \\#1 \\$known_string of function hash_equals expects string, mixed given\\.$#"
count: 1
path: libraries/classes/Middleware/TokenRequestParamChecking.php
-
message: "#^Cannot access offset 'goto' on mixed\\.$#"
count: 1

View File

@ -16,15 +16,6 @@
<code>$value</code>
</MixedAssignment>
</file>
<file src="libraries/classes/Application.php">
<MixedArgument>
<code><![CDATA[$_SESSION[' PMA_token ']]]></code>
</MixedArgument>
<RedundantCast>
<code><![CDATA[(string) $_POST['token']]]></code>
<code><![CDATA[(string) $_POST['token']]]></code>
</RedundantCast>
</file>
<file src="libraries/classes/Bookmark.php">
<DeprecatedMethod>
<code>escapeString</code>
@ -7159,6 +7150,15 @@
<code><![CDATA[(string) $GLOBALS['lang']]]></code>
</RedundantCast>
</file>
<file src="libraries/classes/Middleware/TokenRequestParamChecking.php">
<MixedArgument>
<code><![CDATA[$_SESSION[' PMA_token ']]]></code>
</MixedArgument>
<RedundantCast>
<code><![CDATA[(string) $_POST['token']]]></code>
<code><![CDATA[(string) $_POST['token']]]></code>
</RedundantCast>
</file>
<file src="libraries/classes/Middleware/UrlParamsSetting.php">
<PossiblyInvalidArgument>
<code><![CDATA[$_REQUEST['back']]]></code>
@ -12688,14 +12688,6 @@
<code>providerForTestRules</code>
</PossiblyUnusedMethod>
</file>
<file src="test/classes/ApplicationTest.php">
<RedundantConditionGivenDocblockType>
<code>assertFalse</code>
<code>assertTrue</code>
<code>assertTrue</code>
<code>assertTrue</code>
</RedundantConditionGivenDocblockType>
</file>
<file src="test/classes/BrowseForeignersTest.php">
<MixedArgument>
<code>$result</code>

View File

@ -57,45 +57,4 @@ final class ApplicationTest extends AbstractTestCase
$this->assertSame($expected, $output);
$this->assertSame($errorHandler, $GLOBALS['errorHandler']);
}
public function testCheckTokenRequestParam(): void
{
$application = new Application(
$this->createStub(ErrorHandler::class),
$this->createStub(Config::class),
$this->createStub(Template::class),
new ResponseFactory($this->createStub(ResponseFactoryInterface::class)),
);
$_SERVER['REQUEST_METHOD'] = 'GET';
$application->checkTokenRequestParam();
$this->assertTrue($GLOBALS['token_mismatch']);
$this->assertFalse($GLOBALS['token_provided']);
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['test'] = 'test';
$application->checkTokenRequestParam();
$this->assertTrue($GLOBALS['token_mismatch']);
$this->assertFalse($GLOBALS['token_provided']);
$this->assertArrayNotHasKey('test', $_POST);
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['token'] = 'token';
$_POST['test'] = 'test';
$_SESSION[' PMA_token '] = 'mismatch';
$application->checkTokenRequestParam();
$this->assertTrue($GLOBALS['token_mismatch']);
$this->assertTrue($GLOBALS['token_provided']);
$this->assertArrayNotHasKey('test', $_POST);
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['token'] = 'token';
$_POST['test'] = 'test';
$_SESSION[' PMA_token '] = 'token';
$application->checkTokenRequestParam();
$this->assertFalse($GLOBALS['token_mismatch']);
$this->assertTrue($GLOBALS['token_provided']);
$this->assertArrayHasKey('test', $_POST);
$this->assertEquals('test', $_POST['test']);
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Middleware;
use PhpMyAdmin\Middleware\TokenRequestParamChecking;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(TokenRequestParamChecking::class)]
final class TokenRequestParamCheckingTest extends TestCase
{
public function testCheckTokenRequestParam(): void
{
$_REQUEST = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$middleware = new TokenRequestParamChecking();
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['token'] = 'token';
$_POST['test'] = 'test';
$_SESSION[' PMA_token '] = 'token';
$middleware->checkTokenRequestParam();
$this->assertFalse($GLOBALS['token_mismatch']);
$this->assertTrue($GLOBALS['token_provided']);
$this->assertArrayHasKey('test', $_POST);
$this->assertEquals('test', $_POST['test']);
}
public function testCheckTokenRequestParamWithGetMethod(): void
{
$_REQUEST = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$middleware = new TokenRequestParamChecking();
$_SERVER['REQUEST_METHOD'] = 'GET';
$middleware->checkTokenRequestParam();
$this->assertTrue($GLOBALS['token_mismatch']);
$this->assertFalse($GLOBALS['token_provided']);
}
public function testCheckTokenRequestParamWithoutToken(): void
{
$_REQUEST = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$middleware = new TokenRequestParamChecking();
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['test'] = 'test';
$middleware->checkTokenRequestParam();
$this->assertTrue($GLOBALS['token_mismatch']);
$this->assertFalse($GLOBALS['token_provided']);
$this->assertArrayNotHasKey('test', $_POST);
}
public function testCheckTokenRequestParamWithTokenMismatch(): void
{
$_REQUEST = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$middleware = new TokenRequestParamChecking();
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['token'] = 'token';
$_POST['test'] = 'test';
$_SESSION[' PMA_token '] = 'mismatch';
$middleware->checkTokenRequestParam();
$this->assertTrue($GLOBALS['token_mismatch']);
$this->assertTrue($GLOBALS['token_provided']);
$this->assertArrayNotHasKey('test', $_POST);
}
}