CVE-2021-46876: Insufficient Information (NVD-CWE-noinfo)
value: An issue was discovered in eZ Publish Ibexa Kernel before 7.5.15.1. The /user/sessions endpoint can be abused to determine account existence.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.3
Published: 2023-03-12T05:15Z
Repository: https://github.com/ezsystems/ezpublish-kernel
Language: PHP
Files changed: eZ/Bundle/EzPublishRestBundle/Resources/config/default_settings.yml, eZ/Bundle/EzPublishRestBundle/Resources/config/security.yml, eZ/Publish/Core/REST/Server/Controller/SessionController.php, eZ/Publish/Core/REST/Server/Security/RestAuthenticator.php
Commit date: 2021-03-09 13:43:42 +0100
Commit message: Merge pull request from GHSA-gmrf-99gw-vvwj

Co-authored-by: Bartek Wajda <bartlomiej.wajda@ibexa.co>
Diff: diff --git a/eZ/Bundle/EzPublishRestBundle/Resources/config/default_settings.yml b/eZ/Bundle/EzPublishRestBundle/Resources/config/default_settings.yml
index 65a5a92e18..5a81eb54e3 100644
--- a/eZ/Bundle/EzPublishRestBundle/Resources/config/default_settings.yml
+++ b/eZ/Bundle/EzPublishRestBundle/Resources/config/default_settings.yml
@@ -82,5 +82,9 @@ parameters:
             mediaType: 'UserSession'
             href: 'router.generate("ezpublish_rest_createSession")'
         refreshSession:
             mediaType: 'UserSession'
             href: 'templateRouter.generate("ezpublish_rest_refreshSession", {sessionId: "{sessionId}"})'
+
+    # Boundary times in microseconds which the authentication check will be delayed by.
+    ezpublish_rest.authentication_min_delay_time: 30000
+    ezpublish_rest.authentication_max_delay_time: 500000
\ No newline at end of file
diff --git a/eZ/Bundle/EzPublishRestBundle/Resources/config/security.yml b/eZ/Bundle/EzPublishRestBundle/Resources/config/security.yml
index ab87f69b88..ed9a823c62 100644
--- a/eZ/Bundle/EzPublishRestBundle/Resources/config/security.yml
+++ b/eZ/Bundle/EzPublishRestBundle/Resources/config/security.yml
@@ -13,10 +13,12 @@ services:
             - ~ # Will be replaced at compile time by security provider key
             - "@event_dispatcher"
             - "@ezpublish.config.resolver"
             - "@session.storage"
             - "@?logger"
+            - "%ezpublish_rest.authentication_min_delay_time%"
+            - "%ezpublish_rest.authentication_max_delay_time%"
         abstract: true
 
     ezpublish_rest.security.authentication.logout_handler:
         class: "%ezpublish_rest.security.authentication.logout_handler.class%"
         arguments:
diff --git a/eZ/Publish/Core/REST/Server/Controller/SessionController.php b/eZ/Publish/Core/REST/Server/Controller/SessionController.php
index f3ab2f175d..39136b5b67 100644
--- a/eZ/Publish/Core/REST/Server/Controller/SessionController.php
+++ b/eZ/Publish/Core/REST/Server/Controller/SessionController.php
@@ -57,11 +57,11 @@ class SessionController extends Controller
                 ['Content-Type' => $request->headers->get('Content-Type')],
                 $request->getContent()
             )
         );
         $request->attributes->set('username', $sessionInput->login);
-        $request->attributes->set('password', $sessionInput->password);
+        $request->attributes->set('password', (string) $sessionInput->password);
 
         try {
             $session = $request->getSession();
             if ($session->isStarted() && $this->hasStoredCsrfToken()) {
                 $this->checkCsrfToken($request);
diff --git a/eZ/Publish/Core/REST/Server/Security/RestAuthenticator.php b/eZ/Publish/Core/REST/Server/Security/RestAuthenticator.php
index 9f2c6c67d5..965391be8e 100644
--- a/eZ/Publish/Core/REST/Server/Security/RestAuthenticator.php
+++ b/eZ/Publish/Core/REST/Server/Security/RestAuthenticator.php
@@ -34,10 +34,14 @@ use Symfony\Component\Security\Http\SecurityEvents;
  * Implements \Symfony\Component\Security\Http\Firewall\ListenerInterface to be able to receive the provider key
  * (firewall identifier from configuration).
  */
 class RestAuthenticator implements ListenerInterface, AuthenticatorInterface
 {
+    const DEFAULT_MIN_SLEEP_VALUE = 30000;
+
+    const DEFAULT_MAX_SLEEP_VALUE = 500000;
+
     /** @var \Psr\Log\LoggerInterface */
     private $logger;
 
     /** @var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface */
     private $authenticationManager;
@@ -57,26 +61,40 @@ class RestAuthenticator implements ListenerInterface, AuthenticatorInterface
     private $sessionStorage;
 
     /** @var \Symfony\Component\Security\Http\Logout\LogoutHandlerInterface[] */
     private $logoutHandlers = [];
 
+    /**
+     * @var int|null
+     */
+    private $minSleepTime;
+
+    /**
+     * @var int|null
+     */
+    private $maxSleepTime;
+
     public function __construct(
         TokenStorageInterface $tokenStorage,
         AuthenticationManagerInterface $authenticationManager,
         $providerKey,
         EventDispatcherInterface $dispatcher,
         ConfigResolverInterface $configResolver,
         SessionStorageInterface $sessionStorage,
-        LoggerInterface $logger = null
+        LoggerInterface $logger = null,
+        $minSleepTime = self::DEFAULT_MIN_SLEEP_VALUE,
+        $maxSleepTime = self::DEFAULT_MAX_SLEEP_VALUE
     ) {
         $this->tokenStorage = $tokenStorage;
         $this->authenticationManager = $authenticationManager;
         $this->providerKey = $providerKey;
         $this->dispatcher = $dispatcher;
         $this->configResolver = $configResolver;
         $this->sessionStorage = $sessionStorage;
         $this->logger = $logger;
+        $this->minSleepTime = !is_int($minSleepTime) ? self::DEFAULT_MIN_SLEEP_VALUE : $minSleepTime;
+        $this->maxSleepTime = !is_int($maxSleepTime) ? self::DEFAULT_MAX_SLEEP_VALUE : $maxSleepTime;
     }
 
     /**
      * Doesn't do anything as we don't use this service with main Firewall listener.
      *
@@ -87,10 +105,12 @@ class RestAuthenticator implements ListenerInterface, AuthenticatorInterface
         return;
     }
 
     public function authenticate(Request $request)
     {
+        usleep(random_int($this->minSleepTime, $this->maxSleepTime));
+
         // If a token already exists and username is the same as the one we request authentication for,
         // then return it and mark it as coming from session.
         $previousToken = $this->tokenStorage->getToken();
         if (
             $previousToken instanceof TokenInterface

Vulnerable code:         $request->attributes->set('password', $sessionInput->password);
        LoggerInterface $logger = null
Fixed code: 
    # Boundary times in microseconds which the authentication check will be delayed by.
    ezpublish_rest.authentication_min_delay_time: 30000
    ezpublish_rest.authentication_max_delay_time: 500000
            - "%ezpublish_rest.authentication_min_delay_time%"
            - "%ezpublish_rest.authentication_max_delay_time%"
        $request->attributes->set('password', (string) $sessionInput->password);
    const DEFAULT_MIN_SLEEP_VALUE = 30000;

    const DEFAULT_MAX_SLEEP_VALUE = 500000;

    /**
     * @var int|null
     */
    private $minSleepTime;

    /**
     * @var int|null
     */
    private $maxSleepTime;

        LoggerInterface $logger = null,
        $minSleepTime = self::DEFAULT_MIN_SLEEP_VALUE,
        $maxSleepTime = self::DEFAULT_MAX_SLEEP_VALUE
        $this->minSleepTime = !is_int($minSleepTime) ? self::DEFAULT_MIN_SLEEP_VALUE : $minSleepTime;
        $this->maxSleepTime = !is_int($maxSleepTime) ? self::DEFAULT_MAX_SLEEP_VALUE : $maxSleepTime;
        usleep(random_int($this->minSleepTime, $this->maxSleepTime));