Skip to content

Commit

Permalink
[Security\Core] throw AccessDeniedException when switch user fails
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Nov 12, 2019
1 parent a2f67df commit 290b23a
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 17 deletions.
22 changes: 20 additions & 2 deletions Firewall/SwitchUserListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ public function __invoke(RequestEvent $event)
try {
$this->tokenStorage->setToken($this->attemptSwitchUser($request, $username));
} catch (AuthenticationException $e) {
throw new \LogicException(sprintf('Switch User failed: "%s"', $e->getMessage()));
// Generate 403 in any conditions to prevent user enumeration vulnerabilities
throw new AccessDeniedException('Switch User failed: '.$e->getMessage(), $e);
}
}

Expand Down Expand Up @@ -142,7 +143,24 @@ private function attemptSwitchUser(Request $request, $username)
throw new \LogicException(sprintf('You are already switched to "%s" user.', $token->getUsername()));
}

$user = $this->provider->loadUserByUsername($username);
$currentUsername = $token->getUsername();
$nonExistentUsername = '_'.md5(random_bytes(8).$username);

// To protect against user enumeration via timing measurements
// we always load both successfully and unsuccessfully
try {
$user = $this->provider->loadUserByUsername($username);

try {
$this->provider->loadUserByUsername($nonExistentUsername);
throw new \LogicException('AuthenticationException expected');
} catch (AuthenticationException $e) {
}
} catch (AuthenticationException $e) {
$this->provider->loadUserByUsername($currentUsername);

throw $e;
}

if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) {
$exception = new AccessDeniedException();
Expand Down
62 changes: 47 additions & 15 deletions Tests/Firewall/SwitchUserListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Role\SwitchUserRole;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
Expand Down Expand Up @@ -174,6 +175,7 @@ public function testSwitchUserIsDisallowed()
{
$this->expectException('Symfony\Component\Security\Core\Exception\AccessDeniedException');
$token = new UsernamePasswordToken('username', '', 'key', ['ROLE_FOO']);
$user = new User('username', 'password', []);

$this->tokenStorage->setToken($token);
$this->request->query->set('_switch_user', 'kuba');
Expand All @@ -182,6 +184,31 @@ public function testSwitchUserIsDisallowed()
->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'])
->willReturn(false);

$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['kuba'])
->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException())));

$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
$listener($this->event);
}

public function testSwitchUserTurnsAuthenticationExceptionTo403()
{
$this->expectException('Symfony\Component\Security\Core\Exception\AccessDeniedException');
$token = new UsernamePasswordToken('username', '', 'key', ['ROLE_ALLOWED_TO_SWITCH']);

$this->tokenStorage->setToken($token);
$this->request->query->set('_switch_user', 'kuba');

$this->accessDecisionManager->expects($this->never())
->method('decide');

$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['kuba'], ['username'])
->will($this->onConsecutiveCalls($this->throwException(new UsernameNotFoundException())));

$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
$listener($this->event);
}
Expand All @@ -198,9 +225,10 @@ public function testSwitchUser()
->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user)
->willReturn(true);

$this->userProvider->expects($this->once())
->method('loadUserByUsername')->with('kuba')
->willReturn($user);
$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['kuba'])
->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException())));
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($user);

Expand All @@ -224,9 +252,10 @@ public function testSwitchUserWorksWithFalsyUsernames()
->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'])
->willReturn(true);

$this->userProvider->expects($this->once())
->method('loadUserByUsername')->with('0')
->willReturn($user);
$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['0'])
->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException())));
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($user);

Expand Down Expand Up @@ -254,9 +283,10 @@ public function testSwitchUserKeepsOtherQueryStringParameters()
->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user)
->willReturn(true);

$this->userProvider->expects($this->once())
->method('loadUserByUsername')->with('kuba')
->willReturn($user);
$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['kuba'])
->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException())));
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($user);

Expand All @@ -282,9 +312,10 @@ public function testSwitchUserWithReplacedToken()
->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user)
->willReturn(true);

$this->userProvider->expects($this->any())
->method('loadUserByUsername')->with('kuba')
->willReturn($user);
$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['kuba'])
->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException())));

$dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
$dispatcher
Expand Down Expand Up @@ -329,9 +360,10 @@ public function testSwitchUserStateless()
->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user)
->willReturn(true);

$this->userProvider->expects($this->once())
->method('loadUserByUsername')->with('kuba')
->willReturn($user);
$this->userProvider->expects($this->exactly(2))
->method('loadUserByUsername')
->withConsecutive(['kuba'])
->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException())));
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($user);

Expand Down

0 comments on commit 290b23a

Please sign in to comment.