Prevent user enumeration using Guard or the new Authenticator-based Security
Description
Symfony is a PHP framework for web and console applications and a set of reusable PHP components. The ability to enumerate users was possible without relevant permissions due to different handling depending on whether the user existed or not when attempting to use the switch users functionality. We now ensure that 403s are returned whether the user exists or not if a user cannot switch to a user or if the user does not exist. The patch for this issue is available for branch 3.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
symfony/securityPackagist | >= 5.0.0, < 5.2.8 | 5.2.8 |
symfony/security-guardPackagist | >= 2.8.0, < 3.4.48 | 3.4.48 |
symfony/security-guardPackagist | >= 4.0.0, < 4.4.23 | 4.4.23 |
symfony/security-guardPackagist | >= 5.0.0, < 5.2.8 | 5.2.8 |
symfony/security-corePackagist | >= 2.8.0, < 3.4.48 | 3.4.48 |
symfony/security-corePackagist | >= 4.0.0, < 4.4.23 | 4.4.23 |
symfony/security-corePackagist | >= 5.0.0, < 5.2.8 | 5.2.8 |
lexik/jwt-authentication-bundlePackagist | >= 2.0.0, < 2.10.7 | 2.10.7 |
lexik/jwt-authentication-bundlePackagist | >= 2.11.0, < 2.11.3 | 2.11.3 |
symfony/maker-bundlePackagist | >= 1.27.0, < 1.29.2 | 1.29.2 |
symfony/maker-bundlePackagist | >= 1.30.0, < 1.31.1 | 1.31.1 |
symfony/security-httpPackagist | >= 5.1.0, < 5.2.8 | 5.2.8 |
symfony/securityPackagist | >= 2.8.0, < 3.4.49 | 3.4.49 |
symfony/securityPackagist | >= 4.0.0, < 4.4.24 | 4.4.24 |
symfony/symfonyPackagist | >= 2.8.0, < 3.4.49 | 3.4.49 |
symfony/symfonyPackagist | >= 4.0.0, < 4.4.24 | 4.4.24 |
symfony/symfonyPackagist | >= 5.0.0, < 5.2.9 | 5.2.9 |
Affected products
1- Range: >= 2.8.0, < 3.4.48
Patches
12a581d22cc62security #cve-2021-21424 [Security][Guard] Prevent user enumeration (chalasr)
5 files changed · +71 −7
src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml+2 −1 modified@@ -17,7 +17,7 @@ <argument type="service" id="security.authentication.session_strategy" /> </call> </service> - + <service id="Symfony\Component\Security\Guard\GuardAuthenticatorHandler" alias="security.authentication.guard_handler" /> <!-- See GuardAuthenticationFactory --> @@ -41,6 +41,7 @@ <argument /> <!-- Provider-shared Key --> <argument /> <!-- Authenticator --> <argument type="service" id="logger" on-invalid="null" /> + <argument>%security.authentication.hide_user_not_found%</argument> </service> </services> </container>
src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php+2 −1 modified@@ -13,6 +13,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; @@ -83,7 +84,7 @@ public function authenticate(TokenInterface $token) $this->userChecker->checkPreAuth($user); $this->checkAuthentication($user, $token); $this->userChecker->checkPostAuth($user); - } catch (BadCredentialsException $e) { + } catch (AccountStatusException $e) { if ($this->hideUserNotFoundExceptions) { throw new BadCredentialsException('Bad credentials.', 0, $e); }
src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php+4 −4 modified@@ -79,7 +79,7 @@ public function testAuthenticateWhenProviderDoesNotReturnAnUserInterface() public function testAuthenticateWhenPreChecksFails() { - $this->expectException('Symfony\Component\Security\Core\Exception\CredentialsExpiredException'); + $this->expectException(BadCredentialsException::class); $userChecker = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserCheckerInterface')->getMock(); $userChecker->expects($this->once()) ->method('checkPreAuth') @@ -97,7 +97,7 @@ public function testAuthenticateWhenPreChecksFails() public function testAuthenticateWhenPostChecksFails() { - $this->expectException('Symfony\Component\Security\Core\Exception\AccountExpiredException'); + $this->expectException(BadCredentialsException::class); $userChecker = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserCheckerInterface')->getMock(); $userChecker->expects($this->once()) ->method('checkPostAuth') @@ -116,15 +116,15 @@ public function testAuthenticateWhenPostChecksFails() public function testAuthenticateWhenPostCheckAuthenticationFails() { $this->expectException('Symfony\Component\Security\Core\Exception\BadCredentialsException'); - $this->expectExceptionMessage('Bad credentials'); + $this->expectExceptionMessage('Bad credentials.'); $provider = $this->getProvider(); $provider->expects($this->once()) ->method('retrieveUser') ->willReturn($this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock()) ; $provider->expects($this->once()) ->method('checkAuthentication') - ->willThrowException(new BadCredentialsException()) + ->willThrowException(new CredentialsExpiredException()) ; $provider->authenticate($this->getSupportedToken());
src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php+12 −1 modified@@ -17,7 +17,10 @@ use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; @@ -40,6 +43,7 @@ class GuardAuthenticationListener implements ListenerInterface private $guardAuthenticators; private $logger; private $rememberMeServices; + private $hideUserNotFoundExceptions; /** * @param GuardAuthenticatorHandler $guardHandler The Guard handler @@ -48,7 +52,7 @@ class GuardAuthenticationListener implements ListenerInterface * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider * @param LoggerInterface $logger A LoggerInterface instance */ - public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, $guardAuthenticators, LoggerInterface $logger = null) + public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, $guardAuthenticators, LoggerInterface $logger = null, $hideUserNotFoundExceptions = true) { if (empty($providerKey)) { throw new \InvalidArgumentException('$providerKey must not be empty.'); @@ -59,6 +63,7 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat $this->providerKey = $providerKey; $this->guardAuthenticators = $guardAuthenticators; $this->logger = $logger; + $this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions; } /** @@ -163,6 +168,12 @@ private function executeGuardAuthenticator($uniqueGuardKey, GuardAuthenticatorIn $this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]); } + // Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status) + // to prevent user enumeration via response content + if ($this->hideUserNotFoundExceptions && ($e instanceof UsernameNotFoundException || $e instanceof AccountStatusException)) { + $e = new BadCredentialsException('Bad credentials.', 0, $e); + } + $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey); if ($response instanceof Response) {
src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php+51 −0 modified@@ -16,6 +16,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\LockedException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener; @@ -208,6 +211,54 @@ public function testHandleCatchesAuthenticationException() $listener->handle($this->event); } + /** + * @dataProvider exceptionsToHide + */ + public function testHandleHidesInvalidUserExceptions(AuthenticationException $exceptionToHide) + { + $authenticator = $this->createMock(AuthenticatorInterface::class); + $providerKey = 'my_firewall2'; + + $authenticator + ->expects($this->once()) + ->method('supports') + ->willReturn(true); + $authenticator + ->expects($this->once()) + ->method('getCredentials') + ->willReturn(['username' => 'robin', 'password' => 'hood']); + + $this->authenticationManager + ->expects($this->once()) + ->method('authenticate') + ->willThrowException($exceptionToHide); + + $this->guardAuthenticatorHandler + ->expects($this->once()) + ->method('handleAuthenticationFailure') + ->with($this->callback(function ($e) use ($exceptionToHide) { + return $e instanceof BadCredentialsException && $exceptionToHide === $e->getPrevious(); + }), $this->request, $authenticator, $providerKey); + + $listener = new GuardAuthenticationListener( + $this->guardAuthenticatorHandler, + $this->authenticationManager, + $providerKey, + [$authenticator], + $this->logger + ); + + $listener->handle($this->event); + } + + public function exceptionsToHide() + { + return [ + [new UsernameNotFoundException()], + [new LockedException()], + ]; + } + /** * @group legacy */
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
24- github.com/advisories/GHSA-5pv8-ppvj-4h68ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/KENRNLB3FYXYGDWRBH2PDBOZZKOD7VY4/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/RH7TMM5CHQYBFFGXWRPJDPB3SKCZXI2M/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/UC7BND775DVZDQT3RMGD2HVB2PKLJDJW/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VRUS2H2SSOQWNLBD35SKIWIDQEMV2PD3/mitrevendor-advisory
- nvd.nist.gov/vuln/detail/CVE-2021-21424ghsaADVISORY
- github.com/FriendsOfPHP/security-advisories/blob/master/lexik/jwt-authentication-bundle/CVE-2021-21424.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/symfony/maker-bundle/CVE-2021-21424.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/symfony/security-guard/CVE-2021-21424.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/symfony/security-http/CVE-2021-21424.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/symfony/security/CVE-2021-21424.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/symfony/symfony/CVE-2021-21424.yamlghsaWEB
- github.com/symfony/symfony/commit/2a581d22cc621b33d5464ed65c4bc2057f72f011ghsaWEB
- github.com/symfony/symfony/security/advisories/GHSA-5pv8-ppvj-4h68ghsaWEB
- lists.debian.org/debian-lts-announce/2023/07/msg00014.htmlghsamailing-listWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/KENRNLB3FYXYGDWRBH2PDBOZZKOD7VY4ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/RH7TMM5CHQYBFFGXWRPJDPB3SKCZXI2MghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/UC7BND775DVZDQT3RMGD2HVB2PKLJDJWghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VRUS2H2SSOQWNLBD35SKIWIDQEMV2PD3ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KENRNLB3FYXYGDWRBH2PDBOZZKOD7VY4ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RH7TMM5CHQYBFFGXWRPJDPB3SKCZXI2MghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UC7BND775DVZDQT3RMGD2HVB2PKLJDJWghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/VRUS2H2SSOQWNLBD35SKIWIDQEMV2PD3ghsaWEB
- symfony.com/cve-2021-21424ghsaWEB
News mentions
0No linked articles in our index yet.