Skip to content
This repository was archived by the owner on May 31, 2024. It is now read-only.

Commit 06dbfe4

Browse files
committed
[Security][Guard] Prevent user enumeration via response content
1 parent 7f92437 commit 06dbfe4

File tree

4 files changed

+69
-6
lines changed

4 files changed

+69
-6
lines changed

Core/Authentication/Provider/UserAuthenticationProvider.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1515
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
16+
use Symfony\Component\Security\Core\Exception\AccountStatusException;
1617
use Symfony\Component\Security\Core\Exception\AuthenticationException;
1718
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
1819
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
@@ -83,7 +84,7 @@ public function authenticate(TokenInterface $token)
8384
$this->userChecker->checkPreAuth($user);
8485
$this->checkAuthentication($user, $token);
8586
$this->userChecker->checkPostAuth($user);
86-
} catch (BadCredentialsException $e) {
87+
} catch (AccountStatusException $e) {
8788
if ($this->hideUserNotFoundExceptions) {
8889
throw new BadCredentialsException('Bad credentials.', 0, $e);
8990
}

Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function testAuthenticateWhenProviderDoesNotReturnAnUserInterface()
7979

8080
public function testAuthenticateWhenPreChecksFails()
8181
{
82-
$this->expectException('Symfony\Component\Security\Core\Exception\CredentialsExpiredException');
82+
$this->expectException(BadCredentialsException::class);
8383
$userChecker = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserCheckerInterface')->getMock();
8484
$userChecker->expects($this->once())
8585
->method('checkPreAuth')
@@ -97,7 +97,7 @@ public function testAuthenticateWhenPreChecksFails()
9797

9898
public function testAuthenticateWhenPostChecksFails()
9999
{
100-
$this->expectException('Symfony\Component\Security\Core\Exception\AccountExpiredException');
100+
$this->expectException(BadCredentialsException::class);
101101
$userChecker = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserCheckerInterface')->getMock();
102102
$userChecker->expects($this->once())
103103
->method('checkPostAuth')
@@ -116,15 +116,15 @@ public function testAuthenticateWhenPostChecksFails()
116116
public function testAuthenticateWhenPostCheckAuthenticationFails()
117117
{
118118
$this->expectException('Symfony\Component\Security\Core\Exception\BadCredentialsException');
119-
$this->expectExceptionMessage('Bad credentials');
119+
$this->expectExceptionMessage('Bad credentials.');
120120
$provider = $this->getProvider();
121121
$provider->expects($this->once())
122122
->method('retrieveUser')
123123
->willReturn($this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock())
124124
;
125125
$provider->expects($this->once())
126126
->method('checkAuthentication')
127-
->willThrowException(new BadCredentialsException())
127+
->willThrowException(new CredentialsExpiredException())
128128
;
129129

130130
$provider->authenticate($this->getSupportedToken());

Guard/Firewall/GuardAuthenticationListener.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
1818
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
1919
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
20+
use Symfony\Component\Security\Core\Exception\AccountStatusException;
2021
use Symfony\Component\Security\Core\Exception\AuthenticationException;
22+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
23+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
2124
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
2225
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2326
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
@@ -40,6 +43,7 @@ class GuardAuthenticationListener implements ListenerInterface
4043
private $guardAuthenticators;
4144
private $logger;
4245
private $rememberMeServices;
46+
private $hideUserNotFoundExceptions;
4347

4448
/**
4549
* @param GuardAuthenticatorHandler $guardHandler The Guard handler
@@ -48,7 +52,7 @@ class GuardAuthenticationListener implements ListenerInterface
4852
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider
4953
* @param LoggerInterface $logger A LoggerInterface instance
5054
*/
51-
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, $guardAuthenticators, LoggerInterface $logger = null)
55+
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, $guardAuthenticators, LoggerInterface $logger = null, $hideUserNotFoundExceptions = true)
5256
{
5357
if (empty($providerKey)) {
5458
throw new \InvalidArgumentException('$providerKey must not be empty.');
@@ -59,6 +63,7 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat
5963
$this->providerKey = $providerKey;
6064
$this->guardAuthenticators = $guardAuthenticators;
6165
$this->logger = $logger;
66+
$this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
6267
}
6368

6469
/**
@@ -163,6 +168,12 @@ private function executeGuardAuthenticator($uniqueGuardKey, GuardAuthenticatorIn
163168
$this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]);
164169
}
165170

171+
// Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
172+
// to prevent user enumeration via response content
173+
if ($this->hideUserNotFoundExceptions && ($e instanceof UsernameNotFoundException || $e instanceof AccountStatusException)) {
174+
$e = new BadCredentialsException('Bad credentials.', 0, $e);
175+
}
176+
166177
$response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey);
167178

168179
if ($response instanceof Response) {

Guard/Tests/Firewall/GuardAuthenticationListenerTest.php

+51
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use Symfony\Component\HttpFoundation\Response;
1717
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1818
use Symfony\Component\Security\Core\Exception\AuthenticationException;
19+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
20+
use Symfony\Component\Security\Core\Exception\LockedException;
21+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
1922
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
2023
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2124
use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener;
@@ -208,6 +211,54 @@ public function testHandleCatchesAuthenticationException()
208211
$listener->handle($this->event);
209212
}
210213

214+
/**
215+
* @dataProvider exceptionsToHide
216+
*/
217+
public function testHandleHidesInvalidUserExceptions(AuthenticationException $exceptionToHide)
218+
{
219+
$authenticator = $this->createMock(AuthenticatorInterface::class);
220+
$providerKey = 'my_firewall2';
221+
222+
$authenticator
223+
->expects($this->once())
224+
->method('supports')
225+
->willReturn(true);
226+
$authenticator
227+
->expects($this->once())
228+
->method('getCredentials')
229+
->willReturn(['username' => 'robin', 'password' => 'hood']);
230+
231+
$this->authenticationManager
232+
->expects($this->once())
233+
->method('authenticate')
234+
->willThrowException($exceptionToHide);
235+
236+
$this->guardAuthenticatorHandler
237+
->expects($this->once())
238+
->method('handleAuthenticationFailure')
239+
->with($this->callback(function ($e) use ($exceptionToHide) {
240+
return $e instanceof BadCredentialsException && $exceptionToHide === $e->getPrevious();
241+
}), $this->request, $authenticator, $providerKey);
242+
243+
$listener = new GuardAuthenticationListener(
244+
$this->guardAuthenticatorHandler,
245+
$this->authenticationManager,
246+
$providerKey,
247+
[$authenticator],
248+
$this->logger
249+
);
250+
251+
$listener->handle($this->event);
252+
}
253+
254+
public function exceptionsToHide()
255+
{
256+
return [
257+
[new UsernameNotFoundException()],
258+
[new LockedException()],
259+
];
260+
}
261+
211262
/**
212263
* @group legacy
213264
*/

0 commit comments

Comments
 (0)