Skip to content

Commit d0326e1

Browse files
Merge branch '3.4' into 4.4
* 3.4: [Security][Guard] Prevent user enumeration via response content
2 parents 20f522a + ba995e9 commit d0326e1

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\RequestEvent;
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\AuthenticatorInterface;
2225
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
2326
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
@@ -44,12 +47,13 @@ class GuardAuthenticationListener extends AbstractListener implements ListenerIn
4447
private $guardAuthenticators;
4548
private $logger;
4649
private $rememberMeServices;
50+
private $hideUserNotFoundExceptions;
4751

4852
/**
4953
* @param string $providerKey The provider (i.e. firewall) key
5054
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider
5155
*/
52-
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null)
56+
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null, bool $hideUserNotFoundExceptions = true)
5357
{
5458
if (empty($providerKey)) {
5559
throw new \InvalidArgumentException('$providerKey must not be empty.');
@@ -60,6 +64,7 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat
6064
$this->providerKey = $providerKey;
6165
$this->guardAuthenticators = $guardAuthenticators;
6266
$this->logger = $logger;
67+
$this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
6368
}
6469

6570
/**
@@ -164,6 +169,12 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator
164169
$this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]);
165170
}
166171

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

169180
if ($response instanceof Response) {

Tests/Firewall/GuardAuthenticationListenerTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
2020
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2121
use Symfony\Component\Security\Core\Exception\AuthenticationException;
22+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
23+
use Symfony\Component\Security\Core\Exception\LockedException;
24+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
2225
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2326
use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener;
2427
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
@@ -211,6 +214,54 @@ public function testHandleCatchesAuthenticationException()
211214
$listener($this->event);
212215
}
213216

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

0 commit comments

Comments
 (0)