VYPR
Medium severity5.9GHSA Advisory· Published Sep 3, 2025· Updated Apr 15, 2026

CVE-2025-9824

CVE-2025-9824

Description

ImpactThe attacker can validate if a user exists by checking the time login returns. This timing difference can be used to enumerate valid usernames, after which an attacker could attempt brute force attacks.

PatchesThis vulnerability has been patched, implementing a timing-safe form login authenticator that ensures consistent response times regardless of whether a user exists or not.

Technical DetailsThe vulnerability was caused by different response times when:

  • A valid username was provided (password hashing occurred)
  • An invalid username was provided (no password hashing occurred)

The fix introduces a TimingSafeFormLoginAuthenticator that performs a dummy password hash verification even for non-existent users, ensuring consistent timing.

WorkaroundsNo workarounds are available. Users should upgrade to the patched version.

References * https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mautic/corePackagist
>= 4.4.0, < 4.4.174.4.17
mautic/corePackagist
>= 5.0.0-alpha, < 5.2.85.2.8
mautic/corePackagist
>= 6.0.0-alpha, < 6.0.56.0.5

Affected products

1

Patches

2
6bc4f5f1aabb

Merge commit from fork

https://github.com/mautic/mauticZdeno KuzmanySep 2, 2025via ghsa
5 files changed · +320 0
  • app/bundles/UserBundle/Config/services.php+10 0 modified
    @@ -73,4 +73,14 @@
         $services->alias(LightSaml\SymfonyBridgeBundle\Bridge\Container\BuildContainer::class, 'lightsaml.container.build');
         $services->load('LightSaml\\SpBundle\\Controller\\', '%kernel.project_dir%/vendor/javer/sp-bundle/src/LightSaml/SpBundle/Controller/*.php')
             ->tag('controller.service_arguments');
    +    // Decorate the form_login class to ensure no user enumeration can
    +    // happen via timing attacks.
    +    $services->set('mautic.security.authenticator.form_login.decorator', Mautic\UserBundle\Security\TimingSafeFormLoginAuthenticator::class)
    +        ->decorate('security.authenticator.form_login.main')
    +        ->args([
    +            service('.inner'),
    +            service('mautic.user.provider'),
    +            service('security.password_hasher_factory'),
    +            [], // This will be replaced by the compiler pass
    +        ]);
     };
    
  • app/bundles/UserBundle/DependencyInjection/Compiler/FormLoginAuthenticatorOptionsPass.php+31 0 added
    @@ -0,0 +1,31 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\UserBundle\DependencyInjection\Compiler;
    +
    +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
    +use Symfony\Component\DependencyInjection\ContainerBuilder;
    +
    +class FormLoginAuthenticatorOptionsPass implements CompilerPassInterface
    +{
    +    public function process(ContainerBuilder $container): void
    +    {
    +        if (!$container->has('mautic.security.authenticator.form_login.decorator')) {
    +            return;
    +        }
    +
    +        $decoratedServiceId = 'mautic.security.authenticator.form_login.decorator.inner';
    +        if (!$container->has($decoratedServiceId)) {
    +            return;
    +        }
    +
    +        $decoratedService = $container->getDefinition($decoratedServiceId);
    +        // Grab the options from the original definition
    +        $options          = $decoratedService->getArgument(4);
    +
    +        $decorator = $container->getDefinition('mautic.security.authenticator.form_login.decorator');
    +        // Set the options for our decorated service
    +        $decorator->replaceArgument(3, $options);
    +    }
    +}
    
  • app/bundles/UserBundle/MauticUserBundle.php+2 0 modified
    @@ -2,6 +2,7 @@
     
     namespace Mautic\UserBundle;
     
    +use Mautic\UserBundle\DependencyInjection\Compiler\FormLoginAuthenticatorOptionsPass;
     use Mautic\UserBundle\DependencyInjection\Compiler\OAuthReplacePass;
     use Mautic\UserBundle\DependencyInjection\Compiler\SsoAuthenticatorPass;
     use Mautic\UserBundle\DependencyInjection\Firewall\Factory\MauticSsoFactory;
    @@ -24,5 +25,6 @@ public function build(ContainerBuilder $container): void
     
             $container->addCompilerPass(new OAuthReplacePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
             $container->addCompilerPass(new SsoAuthenticatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
    +        $container->addCompilerPass(new FormLoginAuthenticatorOptionsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
         }
     }
    
  • app/bundles/UserBundle/Security/TimingSafeFormLoginAuthenticator.php+156 0 added
    @@ -0,0 +1,156 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\UserBundle\Security;
    +
    +use Mautic\UserBundle\Entity\User;
    +use Symfony\Component\HttpFoundation\Request;
    +use Symfony\Component\HttpFoundation\Response;
    +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
    +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
    +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    +use Symfony\Component\Security\Core\Exception\AuthenticationException;
    +use Symfony\Component\Security\Core\Exception\UserNotFoundException;
    +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
    +use Symfony\Component\Security\Core\User\UserInterface;
    +use Symfony\Component\Security\Core\User\UserProviderInterface;
    +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
    +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
    +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
    +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
    +use Symfony\Component\Security\Http\SecurityRequestAttributes;
    +
    +class TimingSafeFormLoginAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
    +{
    +    /**
    +     * @var array<mixed>
    +     */
    +    private array $options;
    +
    +    /**
    +     * @param array<mixed> $options
    +     */
    +    public function __construct(private FormLoginAuthenticator $authenticator, private UserProviderInterface $userProvider, private PasswordHasherFactoryInterface $passwordHasherFactory, array $options)
    +    {
    +        $this->authenticator         = $authenticator;
    +        $this->userProvider          = $userProvider;
    +        $this->passwordHasherFactory = $passwordHasherFactory;
    +        $this->options               = array_merge([
    +            'username_parameter' => '_username',
    +            'password_parameter' => '_password',
    +            'check_path'         => '/login_check',
    +            'post_only'          => true,
    +            'form_only'          => false,
    +            'enable_csrf'        => false,
    +            'csrf_parameter'     => '_csrf_token',
    +            'csrf_token_id'      => 'authenticate',
    +        ], $options);
    +    }
    +
    +    public function supports(Request $request): ?bool
    +    {
    +        return $this->authenticator->supports($request);
    +    }
    +
    +    public function authenticate(Request $request): Passport
    +    {
    +        $credentials           = $this->getCredentials($request);
    +        $passwordHasherFactory = $this->passwordHasherFactory;
    +        $userLoader            = function (string $identifier) use ($passwordHasherFactory, $credentials): UserInterface {
    +            try {
    +                // Attempt to load the real user.
    +                return $this->userProvider->loadUserByIdentifier($identifier);
    +            } catch (UserNotFoundException $e) {
    +                // If real user is not found, provide a dummy user and still 'check' the credentials to prevent
    +                // user enumeration via response timing comparison.
    +                // We check it against a pre-calculated hash so the verify functions take roughly
    +                // the same amount of time, and we pass the actual entered password so the response
    +                // timing varies with the given password the same way it does for existing users.
    +                $user = new User();
    +                $passwordHasherFactory->getPasswordHasher($user)->verify('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK', $credentials['password']);
    +                // Rethrow exception
    +                throw $e;
    +            }
    +        };
    +
    +        $userBadge = new UserBadge($credentials['username'], $userLoader);
    +        $passport  = new Passport($userBadge, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]);
    +
    +        if ($this->options['enable_csrf']) {
    +            $passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
    +        }
    +
    +        if ($this->userProvider instanceof PasswordUpgraderInterface) {
    +            $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
    +        }
    +
    +        return $passport;
    +    }
    +
    +    public function createToken(Passport $passport, string $firewallName): TokenInterface
    +    {
    +        return $this->authenticator->createToken($passport, $firewallName);
    +    }
    +
    +    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    +    {
    +        return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
    +    }
    +
    +    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    +    {
    +        return $this->authenticator->onAuthenticationFailure($request, $exception);
    +    }
    +
    +    public function start(Request $request, ?AuthenticationException $authException = null): Response
    +    {
    +        return $this->authenticator->start($request, $authException);
    +    }
    +
    +    public function isInteractive(): bool
    +    {
    +        return $this->authenticator->isInteractive();
    +    }
    +
    +    /**
    +     * @return array<mixed>
    +     */
    +    private function getCredentials(Request $request): array
    +    {
    +        $credentials               = [];
    +        $credentials['csrf_token'] = $request->get($this->options['csrf_parameter']);
    +
    +        if ($this->options['post_only']) {
    +            $credentials['username'] = $request->request->get($this->options['username_parameter']);
    +            $credentials['password'] = $request->request->get($this->options['password_parameter'], '');
    +        } else {
    +            $credentials['username'] = $request->get($this->options['username_parameter']);
    +            $credentials['password'] = $request->get($this->options['password_parameter'], '');
    +        }
    +
    +        if (!\is_string($credentials['username']) && !$credentials['username'] instanceof \Stringable) {
    +            throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
    +        }
    +
    +        $credentials['username'] = trim($credentials['username']);
    +
    +        $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']);
    +
    +        if (!\is_string($credentials['password']) && (!\is_object($credentials['password']) || !method_exists($credentials['password'], '__toString'))) {
    +            throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['password_parameter'], \gettype($credentials['password'])));
    +        }
    +
    +        if (!\is_string($credentials['csrf_token'] ?? '') && (!\is_object($credentials['csrf_token']) || !method_exists($credentials['csrf_token'], '__toString'))) {
    +            throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['csrf_parameter'], \gettype($credentials['csrf_token'])));
    +        }
    +
    +        return $credentials;
    +    }
    +}
    
  • app/bundles/UserBundle/Tests/Security/Authenticator/TimingSafeFormLoginAuthenticatorTest.php+121 0 added
    @@ -0,0 +1,121 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\UserBundle\Tests\Security\Authenticator;
    +
    +use Mautic\UserBundle\Entity\User;
    +use Mautic\UserBundle\Security\TimingSafeFormLoginAuthenticator;
    +use PHPUnit\Framework\TestCase;
    +use Symfony\Component\HttpFoundation\Request;
    +use Symfony\Component\HttpFoundation\Session\Session;
    +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
    +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
    +use Symfony\Component\PasswordHasher\PasswordHasherInterface;
    +use Symfony\Component\Security\Core\Exception\UserNotFoundException;
    +use Symfony\Component\Security\Core\User\UserProviderInterface;
    +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
    +
    +class TimingSafeFormLoginAuthenticatorTest extends TestCase
    +{
    +    /**
    +     * @return array<mixed>
    +     */
    +    private function getCredentials(TimingSafeFormLoginAuthenticator $authenticator, Request $request): array
    +    {
    +        $method = new \ReflectionMethod(TimingSafeFormLoginAuthenticator::class, 'getCredentials');
    +        $method->setAccessible(true);
    +
    +        return $method->invoke($authenticator, $request);
    +    }
    +
    +    public function testAuthenticateWithExistingUser(): void
    +    {
    +        $request = new Request([], ['username' => 'testuser', 'password' => 'password']);
    +        $request->setSession(new Session(new MockArraySessionStorage()));
    +        $user    = new User();
    +        $user->setUsername('testuser');
    +
    +        /** @var UserProviderInterface|\PHPUnit\Framework\MockObject\MockObject $userProvider */
    +        $userProvider = $this->createMock(UserProviderInterface::class);
    +        $userProvider->expects($this->once())
    +            ->method('loadUserByIdentifier')
    +            ->with('testuser')
    +            ->willReturn($user);
    +
    +        $passwordHasher = $this->createMock(PasswordHasherInterface::class);
    +        /** @var PasswordHasherFactoryInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasherFactory */
    +        $passwordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
    +        $passwordHasherFactory->expects($this->never())
    +            ->method('getPasswordHasher');
    +
    +        /** @var FormLoginAuthenticator|\PHPUnit\Framework\MockObject\MockObject $formLoginAuthenticator */
    +        $formLoginAuthenticator = $this->createMock(FormLoginAuthenticator::class);
    +
    +        $authenticator = new TimingSafeFormLoginAuthenticator(
    +            $formLoginAuthenticator,
    +            $userProvider,
    +            $passwordHasherFactory,
    +            [
    +                'enable_csrf'        => false,
    +                'username_parameter' => 'username',
    +                'password_parameter' => 'password',
    +                'csrf_parameter'     => '_csrf_token',
    +                'post_only'          => true,
    +            ]
    +        );
    +
    +        $credentials = $this->getCredentials($authenticator, $request);
    +        $this->assertEquals('testuser', $credentials['username']);
    +        $this->assertEquals('password', $credentials['password']);
    +
    +        $passport = $authenticator->authenticate($request);
    +        $passport->getUser();
    +    }
    +
    +    public function testAuthenticateWithNonExistingUser(): void
    +    {
    +        $this->expectException(UserNotFoundException::class);
    +
    +        $request = new Request([], ['username' => 'testuser', 'password' => 'password']);
    +        $request->setSession(new Session(new MockArraySessionStorage()));
    +
    +        /** @var UserProviderInterface|\PHPUnit\Framework\MockObject\MockObject $userProvider */
    +        $userProvider = $this->createMock(UserProviderInterface::class);
    +        $userProvider->expects($this->once())
    +            ->method('loadUserByIdentifier')
    +            ->with('testuser')
    +            ->willThrowException(new UserNotFoundException());
    +
    +        /** @var PasswordHasherInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasher */
    +        $passwordHasher = $this->createMock(PasswordHasherInterface::class);
    +        $passwordHasher->expects($this->once())
    +            ->method('verify')
    +            ->with('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK', 'password');
    +
    +        /** @var PasswordHasherFactoryInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasherFactory */
    +        $passwordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
    +        $passwordHasherFactory->expects($this->once())
    +            ->method('getPasswordHasher')
    +            ->willReturn($passwordHasher);
    +
    +        /** @var FormLoginAuthenticator|\PHPUnit\Framework\MockObject\MockObject $formLoginAuthenticator */
    +        $formLoginAuthenticator = $this->createMock(FormLoginAuthenticator::class);
    +
    +        $authenticator = new TimingSafeFormLoginAuthenticator(
    +            $formLoginAuthenticator,
    +            $userProvider,
    +            $passwordHasherFactory,
    +            [
    +                'enable_csrf'        => false,
    +                'username_parameter' => 'username',
    +                'password_parameter' => 'password',
    +                'csrf_parameter'     => '_csrf_token',
    +                'post_only'          => true,
    +            ]
    +        );
    +
    +        $passport = $authenticator->authenticate($request);
    +        $passport->getUser();
    +    }
    +}
    
b4264c717ce3

MST-46 - Prevent user enumeration

https://github.com/mautic/mauticNick VanpraetJul 25, 2025via ghsa
5 files changed · +320 0
  • app/bundles/UserBundle/Config/services.php+10 0 modified
    @@ -73,4 +73,14 @@
         $services->alias(LightSaml\SymfonyBridgeBundle\Bridge\Container\BuildContainer::class, 'lightsaml.container.build');
         $services->load('LightSaml\\SpBundle\\Controller\\', '%kernel.project_dir%/vendor/javer/sp-bundle/src/LightSaml/SpBundle/Controller/*.php')
             ->tag('controller.service_arguments');
    +    // Decorate the form_login class to ensure no user enumeration can
    +    // happen via timing attacks.
    +    $services->set('mautic.security.authenticator.form_login.decorator', Mautic\UserBundle\Security\TimingSafeFormLoginAuthenticator::class)
    +        ->decorate('security.authenticator.form_login.main')
    +        ->args([
    +            service('.inner'),
    +            service('mautic.user.provider'),
    +            service('security.password_hasher_factory'),
    +            [], // This will be replaced by the compiler pass
    +        ]);
     };
    
  • app/bundles/UserBundle/DependencyInjection/Compiler/FormLoginAuthenticatorOptionsPass.php+31 0 added
    @@ -0,0 +1,31 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\UserBundle\DependencyInjection\Compiler;
    +
    +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
    +use Symfony\Component\DependencyInjection\ContainerBuilder;
    +
    +class FormLoginAuthenticatorOptionsPass implements CompilerPassInterface
    +{
    +    public function process(ContainerBuilder $container): void
    +    {
    +        if (!$container->has('mautic.security.authenticator.form_login.decorator')) {
    +            return;
    +        }
    +
    +        $decoratedServiceId = 'mautic.security.authenticator.form_login.decorator.inner';
    +        if (!$container->has($decoratedServiceId)) {
    +            return;
    +        }
    +
    +        $decoratedService = $container->getDefinition($decoratedServiceId);
    +        // Grab the options from the original definition
    +        $options          = $decoratedService->getArgument(4);
    +
    +        $decorator = $container->getDefinition('mautic.security.authenticator.form_login.decorator');
    +        // Set the options for our decorated service
    +        $decorator->replaceArgument(3, $options);
    +    }
    +}
    
  • app/bundles/UserBundle/MauticUserBundle.php+2 0 modified
    @@ -2,6 +2,7 @@
     
     namespace Mautic\UserBundle;
     
    +use Mautic\UserBundle\DependencyInjection\Compiler\FormLoginAuthenticatorOptionsPass;
     use Mautic\UserBundle\DependencyInjection\Compiler\OAuthReplacePass;
     use Mautic\UserBundle\DependencyInjection\Compiler\SsoAuthenticatorPass;
     use Mautic\UserBundle\DependencyInjection\Firewall\Factory\MauticSsoFactory;
    @@ -24,5 +25,6 @@ public function build(ContainerBuilder $container): void
     
             $container->addCompilerPass(new OAuthReplacePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
             $container->addCompilerPass(new SsoAuthenticatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
    +        $container->addCompilerPass(new FormLoginAuthenticatorOptionsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
         }
     }
    
  • app/bundles/UserBundle/Security/TimingSafeFormLoginAuthenticator.php+156 0 added
    @@ -0,0 +1,156 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\UserBundle\Security;
    +
    +use Mautic\UserBundle\Entity\User;
    +use Symfony\Component\HttpFoundation\Request;
    +use Symfony\Component\HttpFoundation\Response;
    +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
    +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
    +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    +use Symfony\Component\Security\Core\Exception\AuthenticationException;
    +use Symfony\Component\Security\Core\Exception\UserNotFoundException;
    +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
    +use Symfony\Component\Security\Core\User\UserInterface;
    +use Symfony\Component\Security\Core\User\UserProviderInterface;
    +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
    +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
    +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
    +use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
    +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
    +use Symfony\Component\Security\Http\SecurityRequestAttributes;
    +
    +class TimingSafeFormLoginAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
    +{
    +    /**
    +     * @var array<mixed>
    +     */
    +    private array $options;
    +
    +    /**
    +     * @param array<mixed> $options
    +     */
    +    public function __construct(private FormLoginAuthenticator $authenticator, private UserProviderInterface $userProvider, private PasswordHasherFactoryInterface $passwordHasherFactory, array $options)
    +    {
    +        $this->authenticator         = $authenticator;
    +        $this->userProvider          = $userProvider;
    +        $this->passwordHasherFactory = $passwordHasherFactory;
    +        $this->options               = array_merge([
    +            'username_parameter' => '_username',
    +            'password_parameter' => '_password',
    +            'check_path'         => '/login_check',
    +            'post_only'          => true,
    +            'form_only'          => false,
    +            'enable_csrf'        => false,
    +            'csrf_parameter'     => '_csrf_token',
    +            'csrf_token_id'      => 'authenticate',
    +        ], $options);
    +    }
    +
    +    public function supports(Request $request): ?bool
    +    {
    +        return $this->authenticator->supports($request);
    +    }
    +
    +    public function authenticate(Request $request): Passport
    +    {
    +        $credentials           = $this->getCredentials($request);
    +        $passwordHasherFactory = $this->passwordHasherFactory;
    +        $userLoader            = function (string $identifier) use ($passwordHasherFactory, $credentials): UserInterface {
    +            try {
    +                // Attempt to load the real user.
    +                return $this->userProvider->loadUserByIdentifier($identifier);
    +            } catch (UserNotFoundException $e) {
    +                // If real user is not found, provide a dummy user and still 'check' the credentials to prevent
    +                // user enumeration via response timing comparison.
    +                // We check it against a pre-calculated hash so the verify functions take roughly
    +                // the same amount of time, and we pass the actual entered password so the response
    +                // timing varies with the given password the same way it does for existing users.
    +                $user = new User();
    +                $passwordHasherFactory->getPasswordHasher($user)->verify('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK', $credentials['password']);
    +                // Rethrow exception
    +                throw $e;
    +            }
    +        };
    +
    +        $userBadge = new UserBadge($credentials['username'], $userLoader);
    +        $passport  = new Passport($userBadge, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]);
    +
    +        if ($this->options['enable_csrf']) {
    +            $passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
    +        }
    +
    +        if ($this->userProvider instanceof PasswordUpgraderInterface) {
    +            $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
    +        }
    +
    +        return $passport;
    +    }
    +
    +    public function createToken(Passport $passport, string $firewallName): TokenInterface
    +    {
    +        return $this->authenticator->createToken($passport, $firewallName);
    +    }
    +
    +    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    +    {
    +        return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
    +    }
    +
    +    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    +    {
    +        return $this->authenticator->onAuthenticationFailure($request, $exception);
    +    }
    +
    +    public function start(Request $request, AuthenticationException $authException = null): Response
    +    {
    +        return $this->authenticator->start($request, $authException);
    +    }
    +
    +    public function isInteractive(): bool
    +    {
    +        return $this->authenticator->isInteractive();
    +    }
    +
    +    /**
    +     * @return array<mixed>
    +     */
    +    private function getCredentials(Request $request): array
    +    {
    +        $credentials               = [];
    +        $credentials['csrf_token'] = $request->get($this->options['csrf_parameter']);
    +
    +        if ($this->options['post_only']) {
    +            $credentials['username'] = $request->request->get($this->options['username_parameter']);
    +            $credentials['password'] = $request->request->get($this->options['password_parameter'], '');
    +        } else {
    +            $credentials['username'] = $request->get($this->options['username_parameter']);
    +            $credentials['password'] = $request->get($this->options['password_parameter'], '');
    +        }
    +
    +        if (!\is_string($credentials['username']) && !$credentials['username'] instanceof \Stringable) {
    +            throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
    +        }
    +
    +        $credentials['username'] = trim($credentials['username']);
    +
    +        $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']);
    +
    +        if (!\is_string($credentials['password']) && (!\is_object($credentials['password']) || !method_exists($credentials['password'], '__toString'))) {
    +            throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['password_parameter'], \gettype($credentials['password'])));
    +        }
    +
    +        if (!\is_string($credentials['csrf_token'] ?? '') && (!\is_object($credentials['csrf_token']) || !method_exists($credentials['csrf_token'], '__toString'))) {
    +            throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['csrf_parameter'], \gettype($credentials['csrf_token'])));
    +        }
    +
    +        return $credentials;
    +    }
    +}
    
  • app/bundles/UserBundle/Tests/Security/Authenticator/TimingSafeFormLoginAuthenticatorTest.php+121 0 added
    @@ -0,0 +1,121 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Mautic\UserBundle\Tests\Security\Authenticator;
    +
    +use Mautic\UserBundle\Entity\User;
    +use Mautic\UserBundle\Security\TimingSafeFormLoginAuthenticator;
    +use PHPUnit\Framework\TestCase;
    +use Symfony\Component\HttpFoundation\Request;
    +use Symfony\Component\HttpFoundation\Session\Session;
    +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
    +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
    +use Symfony\Component\PasswordHasher\PasswordHasherInterface;
    +use Symfony\Component\Security\Core\Exception\UserNotFoundException;
    +use Symfony\Component\Security\Core\User\UserProviderInterface;
    +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
    +
    +class TimingSafeFormLoginAuthenticatorTest extends TestCase
    +{
    +    /**
    +     * @return array<mixed>
    +     */
    +    private function getCredentials(TimingSafeFormLoginAuthenticator $authenticator, Request $request): array
    +    {
    +        $method = new \ReflectionMethod(TimingSafeFormLoginAuthenticator::class, 'getCredentials');
    +        $method->setAccessible(true);
    +
    +        return $method->invoke($authenticator, $request);
    +    }
    +
    +    public function testAuthenticateWithExistingUser(): void
    +    {
    +        $request = new Request([], ['username' => 'testuser', 'password' => 'password']);
    +        $request->setSession(new Session(new MockArraySessionStorage()));
    +        $user    = new User();
    +        $user->setUsername('testuser');
    +
    +        /** @var UserProviderInterface|\PHPUnit\Framework\MockObject\MockObject $userProvider */
    +        $userProvider = $this->createMock(UserProviderInterface::class);
    +        $userProvider->expects($this->once())
    +            ->method('loadUserByIdentifier')
    +            ->with('testuser')
    +            ->willReturn($user);
    +
    +        $passwordHasher = $this->createMock(PasswordHasherInterface::class);
    +        /** @var PasswordHasherFactoryInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasherFactory */
    +        $passwordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
    +        $passwordHasherFactory->expects($this->never())
    +            ->method('getPasswordHasher');
    +
    +        /** @var FormLoginAuthenticator|\PHPUnit\Framework\MockObject\MockObject $formLoginAuthenticator */
    +        $formLoginAuthenticator = $this->createMock(FormLoginAuthenticator::class);
    +
    +        $authenticator = new TimingSafeFormLoginAuthenticator(
    +            $formLoginAuthenticator,
    +            $userProvider,
    +            $passwordHasherFactory,
    +            [
    +                'enable_csrf'        => false,
    +                'username_parameter' => 'username',
    +                'password_parameter' => 'password',
    +                'csrf_parameter'     => '_csrf_token',
    +                'post_only'          => true,
    +            ]
    +        );
    +
    +        $credentials = $this->getCredentials($authenticator, $request);
    +        $this->assertEquals('testuser', $credentials['username']);
    +        $this->assertEquals('password', $credentials['password']);
    +
    +        $passport = $authenticator->authenticate($request);
    +        $passport->getUser();
    +    }
    +
    +    public function testAuthenticateWithNonExistingUser(): void
    +    {
    +        $this->expectException(UserNotFoundException::class);
    +
    +        $request = new Request([], ['username' => 'testuser', 'password' => 'password']);
    +        $request->setSession(new Session(new MockArraySessionStorage()));
    +
    +        /** @var UserProviderInterface|\PHPUnit\Framework\MockObject\MockObject $userProvider */
    +        $userProvider = $this->createMock(UserProviderInterface::class);
    +        $userProvider->expects($this->once())
    +            ->method('loadUserByIdentifier')
    +            ->with('testuser')
    +            ->willThrowException(new UserNotFoundException());
    +
    +        /** @var PasswordHasherInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasher */
    +        $passwordHasher = $this->createMock(PasswordHasherInterface::class);
    +        $passwordHasher->expects($this->once())
    +            ->method('verify')
    +            ->with('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK', 'password');
    +
    +        /** @var PasswordHasherFactoryInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasherFactory */
    +        $passwordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
    +        $passwordHasherFactory->expects($this->once())
    +            ->method('getPasswordHasher')
    +            ->willReturn($passwordHasher);
    +
    +        /** @var FormLoginAuthenticator|\PHPUnit\Framework\MockObject\MockObject $formLoginAuthenticator */
    +        $formLoginAuthenticator = $this->createMock(FormLoginAuthenticator::class);
    +
    +        $authenticator = new TimingSafeFormLoginAuthenticator(
    +            $formLoginAuthenticator,
    +            $userProvider,
    +            $passwordHasherFactory,
    +            [
    +                'enable_csrf'        => false,
    +                'username_parameter' => 'username',
    +                'password_parameter' => 'password',
    +                'csrf_parameter'     => '_csrf_token',
    +                'post_only'          => true,
    +            ]
    +        );
    +
    +        $passport = $authenticator->authenticate($request);
    +        $passport->getUser();
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.