Skip to content

Commit d5c0fba

Browse files
Merge branch '3.4' into 4.4
* 3.4: [Security][Guard] Prevent user enumeration via response content
2 parents a0f8fc5 + 2a581d2 commit d5c0fba

File tree

5 files changed

+70
-6
lines changed

5 files changed

+70
-6
lines changed

src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<argument type="service" id="security.authentication.session_strategy" />
1818
</call>
1919
</service>
20-
20+
2121
<service id="Symfony\Component\Security\Guard\GuardAuthenticatorHandler" alias="security.authentication.guard_handler" />
2222

2323
<!-- See GuardAuthenticationFactory -->
@@ -42,6 +42,7 @@
4242
<argument /> <!-- Provider-shared Key -->
4343
<argument /> <!-- Authenticator -->
4444
<argument type="service" id="logger" on-invalid="null" />
45+
<argument>%security.authentication.hide_user_not_found%</argument>
4546
</service>
4647
</services>
4748
</container>

src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
1515
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1616
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
17+
use Symfony\Component\Security\Core\Exception\AccountStatusException;
1718
use Symfony\Component\Security\Core\Exception\AuthenticationException;
1819
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
1920
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
@@ -80,7 +81,7 @@ public function authenticate(TokenInterface $token)
8081
$this->userChecker->checkPreAuth($user);
8182
$this->checkAuthentication($user, $token);
8283
$this->userChecker->checkPostAuth($user);
83-
} catch (BadCredentialsException $e) {
84+
} catch (AccountStatusException $e) {
8485
if ($this->hideUserNotFoundExceptions) {
8586
throw new BadCredentialsException('Bad credentials.', 0, $e);
8687
}

src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public function testAuthenticateWhenProviderDoesNotReturnAnUserInterface()
8383

8484
public function testAuthenticateWhenPreChecksFails()
8585
{
86-
$this->expectException(CredentialsExpiredException::class);
86+
$this->expectException(BadCredentialsException::class);
8787
$userChecker = $this->createMock(UserCheckerInterface::class);
8888
$userChecker->expects($this->once())
8989
->method('checkPreAuth')
@@ -101,7 +101,7 @@ public function testAuthenticateWhenPreChecksFails()
101101

102102
public function testAuthenticateWhenPostChecksFails()
103103
{
104-
$this->expectException(AccountExpiredException::class);
104+
$this->expectException(BadCredentialsException::class);
105105
$userChecker = $this->createMock(UserCheckerInterface::class);
106106
$userChecker->expects($this->once())
107107
->method('checkPostAuth')
@@ -128,7 +128,7 @@ public function testAuthenticateWhenPostCheckAuthenticationFails()
128128
;
129129
$provider->expects($this->once())
130130
->method('checkAuthentication')
131-
->willThrowException(new BadCredentialsException())
131+
->willThrowException(new CredentialsExpiredException())
132132
;
133133

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

src/Symfony/Component/Security/Guard/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) {

src/Symfony/Component/Security/Guard/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)