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

Commit

Permalink
[Security][Guard] Prevent user enumeration via response content
Browse files Browse the repository at this point in the history
  • Loading branch information
chalasr committed Jan 29, 2021
1 parent 7f92437 commit 06dbfe4
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 6 deletions.
3 changes: 2 additions & 1 deletion Core/Authentication/Provider/UserAuthenticationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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());
Expand Down
13 changes: 12 additions & 1 deletion Guard/Firewall/GuardAuthenticationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +43,7 @@ class GuardAuthenticationListener implements ListenerInterface
private $guardAuthenticators;
private $logger;
private $rememberMeServices;
private $hideUserNotFoundExceptions;

/**
* @param GuardAuthenticatorHandler $guardHandler The Guard handler
Expand All @@ -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.');
Expand All @@ -59,6 +63,7 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat
$this->providerKey = $providerKey;
$this->guardAuthenticators = $guardAuthenticators;
$this->logger = $logger;
$this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
51 changes: 51 additions & 0 deletions Guard/Tests/Firewall/GuardAuthenticationListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down

0 comments on commit 06dbfe4

Please sign in to comment.