diff --git a/libraries/classes/Application.php b/libraries/classes/Application.php index d132c97cfb..35204cbdcf 100644 --- a/libraries/classes/Application.php +++ b/libraries/classes/Application.php @@ -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']); - } } diff --git a/libraries/classes/Middleware/DatabaseAndTableSetting.php b/libraries/classes/Middleware/DatabaseAndTableSetting.php index 7733d815ed..31f25ff45b 100644 --- a/libraries/classes/Middleware/DatabaseAndTableSetting.php +++ b/libraries/classes/Middleware/DatabaseAndTableSetting.php @@ -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']); + } } diff --git a/libraries/classes/Middleware/PhpExtensionsChecking.php b/libraries/classes/Middleware/PhpExtensionsChecking.php index d450427ffe..767afa156e 100644 --- a/libraries/classes/Middleware/PhpExtensionsChecking.php +++ b/libraries/classes/Middleware/PhpExtensionsChecking.php @@ -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); + } } diff --git a/libraries/classes/Middleware/TokenRequestParamChecking.php b/libraries/classes/Middleware/TokenRequestParamChecking.php index ad20b13023..7a482ee3bc 100644 --- a/libraries/classes/Middleware/TokenRequestParamChecking.php +++ b/libraries/classes/Middleware/TokenRequestParamChecking.php @@ -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); + } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a1694aaa44..9d647d2ef5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 976a63e642..ae3c12d174 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -16,15 +16,6 @@ $value - - - - - - - - - escapeString @@ -7159,6 +7150,15 @@ + + + + + + + + + @@ -12688,14 +12688,6 @@ providerForTestRules - - - assertFalse - assertTrue - assertTrue - assertTrue - - $result diff --git a/test/classes/ApplicationTest.php b/test/classes/ApplicationTest.php index 0578a30fda..e6488bdc77 100644 --- a/test/classes/ApplicationTest.php +++ b/test/classes/ApplicationTest.php @@ -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']); - } } diff --git a/test/classes/Middleware/TokenRequestParamCheckingTest.php b/test/classes/Middleware/TokenRequestParamCheckingTest.php new file mode 100644 index 0000000000..6c5004cbfb --- /dev/null +++ b/test/classes/Middleware/TokenRequestParamCheckingTest.php @@ -0,0 +1,84 @@ +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); + } +}