Skip to content

Commit 8137325

Browse files
Merge branch '4.4' into 5.2
* 4.4: [CI][Psalm] Install stable/released PHPUnit [Security] Add missing Finnish translations [Security][Guard] Prevent user enumeration via response content
2 parents a191352 + d0326e1 commit 8137325

File tree

2 files changed

+64
-1
lines changed

2 files changed

+64
-1
lines changed

Firewall/GuardAuthenticationListener.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
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\CustomUserMessageAccountStatusException;
24+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
2125
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2226
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
2327
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
@@ -40,12 +44,13 @@ class GuardAuthenticationListener extends AbstractListener
4044
private $guardAuthenticators;
4145
private $logger;
4246
private $rememberMeServices;
47+
private $hideUserNotFoundExceptions;
4348

4449
/**
4550
* @param string $providerKey The provider (i.e. firewall) key
4651
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider
4752
*/
48-
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null)
53+
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null, bool $hideUserNotFoundExceptions = true)
4954
{
5055
if (empty($providerKey)) {
5156
throw new \InvalidArgumentException('$providerKey must not be empty.');
@@ -56,6 +61,7 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat
5661
$this->providerKey = $providerKey;
5762
$this->guardAuthenticators = $guardAuthenticators;
5863
$this->logger = $logger;
64+
$this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
5965
}
6066

6167
/**
@@ -160,6 +166,12 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator
160166
$this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]);
161167
}
162168

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

165177
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)