Skip to content

Commit ba995e9

Browse files
committed
[Security][Guard] Prevent user enumeration via response content
1 parent 65012d7 commit ba995e9

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

Firewall/GuardAuthenticationListener.php

Lines changed: 12 additions & 1 deletion
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) {

Tests/Firewall/GuardAuthenticationListenerTest.php

Lines changed: 51 additions & 0 deletions
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)