Merge branch '4.0' into 4.1

* 4.0:
  clear CSRF tokens when the user is logged out
This commit is contained in:
Fabien Potencier 2018-05-24 15:20:06 +02:00
commit 3626bd1214
14 changed files with 326 additions and 3 deletions

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class RegisterCsrfTokenClearingLogoutHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('security.logout_listener') || !$container->has('security.csrf.token_storage')) {
return;
}
$csrfTokenStorage = $container->findDefinition('security.csrf.token_storage');
$csrfTokenStorageClass = $container->getParameterBag()->resolveValue($csrfTokenStorage->getClass());
if (!is_subclass_of($csrfTokenStorageClass, 'Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface')) {
return;
}
$container->register('security.logout.handler.csrf_token_clearing', 'Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler')
->addArgument(new Reference('security.csrf.token_storage'))
->setPublic(false);
$container->findDefinition('security.logout_listener')->addMethodCall('addHandler', array(new Reference('security.logout.handler.csrf_token_clearing')));
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -59,6 +60,7 @@ class SecurityBundle extends Bundle
$extension->addUserProviderFactory(new LdapFactory());
$container->addCompilerPass(new AddSecurityVotersPass());
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass());
}
public function registerCommands(Application $application)

View File

@ -31,4 +31,22 @@ class LogoutTest extends WebTestCase
$this->assertNull($cookieJar->get('REMEMBERME'));
}
public function testCsrfTokensAreClearedOnLogout()
{
$client = $this->createClient(array('test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml'));
$client->getContainer()->get('security.csrf.token_storage')->setToken('foo', 'bar');
$client->request('POST', '/login', array(
'_username' => 'johannes',
'_password' => 'test',
));
$this->assertTrue($client->getContainer()->get('security.csrf.token_storage')->hasToken('foo'));
$this->assertSame('bar', $client->getContainer()->get('security.csrf.token_storage')->getToken('foo'));
$client->request('GET', '/logout');
$this->assertFalse($client->getContainer()->get('security.csrf.token_storage')->hasToken('foo'));
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
return array(
new FrameworkBundle(),
new SecurityBundle(),
);

View File

@ -0,0 +1,26 @@
imports:
- { resource: ./../config/framework.yml }
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
providers:
in_memory:
memory:
users:
johannes: { password: test, roles: [ROLE_USER] }
firewalls:
default:
form_login:
check_path: login
remember_me: true
require_previous_session: false
remember_me:
always_remember_me: true
key: key
logout:
invalidate_session: false
anonymous: ~
stateless: true

View File

@ -0,0 +1,5 @@
login:
path: /login
logout:
path: /logout

View File

@ -113,4 +113,32 @@ class NativeSessionTokenStorageTest extends TestCase
$this->assertSame('TOKEN', $this->storage->removeToken('token_id'));
$this->assertFalse($this->storage->hasToken('token_id'));
}
public function testClearRemovesAllTokensFromTheConfiguredNamespace()
{
$this->storage->setToken('foo', 'bar');
$this->storage->clear();
$this->assertFalse($this->storage->hasToken('foo'));
$this->assertArrayNotHasKey(self::SESSION_NAMESPACE, $_SESSION);
}
public function testClearDoesNotRemoveSessionValuesFromOtherNamespaces()
{
$_SESSION['foo']['bar'] = 'baz';
$this->storage->clear();
$this->assertArrayHasKey('foo', $_SESSION);
$this->assertArrayHasKey('bar', $_SESSION['foo']);
$this->assertSame('baz', $_SESSION['foo']['bar']);
}
public function testClearDoesNotRemoveNonNamespacedSessionValues()
{
$_SESSION['foo'] = 'baz';
$this->storage->clear();
$this->assertArrayHasKey('foo', $_SESSION);
$this->assertSame('baz', $_SESSION['foo']);
}
}

View File

@ -129,4 +129,31 @@ class SessionTokenStorageTest extends TestCase
$this->assertSame('TOKEN', $this->storage->removeToken('token_id'));
}
public function testClearRemovesAllTokensFromTheConfiguredNamespace()
{
$this->storage->setToken('foo', 'bar');
$this->storage->clear();
$this->assertFalse($this->storage->hasToken('foo'));
$this->assertFalse($this->session->has(self::SESSION_NAMESPACE.'/foo'));
}
public function testClearDoesNotRemoveSessionValuesFromOtherNamespaces()
{
$this->session->set('foo/bar', 'baz');
$this->storage->clear();
$this->assertTrue($this->session->has('foo/bar'));
$this->assertSame('baz', $this->session->get('foo/bar'));
}
public function testClearDoesNotRemoveNonNamespacedSessionValues()
{
$this->session->set('foo', 'baz');
$this->storage->clear();
$this->assertTrue($this->session->has('foo'));
$this->assertSame('baz', $this->session->get('foo'));
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Csrf\TokenStorage;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
interface ClearableTokenStorageInterface extends TokenStorageInterface
{
/**
* Removes all CSRF tokens.
*/
public function clear();
}

View File

@ -18,7 +18,7 @@ use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NativeSessionTokenStorage implements TokenStorageInterface
class NativeSessionTokenStorage implements ClearableTokenStorageInterface
{
/**
* The namespace used to store values in the session.
@ -102,6 +102,14 @@ class NativeSessionTokenStorage implements TokenStorageInterface
return $token;
}
/**
* {@inheritdoc}
*/
public function clear()
{
unset($_SESSION[$this->namespace]);
}
private function startSession()
{
if (PHP_SESSION_NONE === session_status()) {

View File

@ -19,7 +19,7 @@ use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SessionTokenStorage implements TokenStorageInterface
class SessionTokenStorage implements ClearableTokenStorageInterface
{
/**
* The namespace used to store values in the session.
@ -92,4 +92,16 @@ class SessionTokenStorage implements TokenStorageInterface
return $this->session->remove($this->namespace.'/'.$tokenId);
}
/**
* {@inheritdoc}
*/
public function clear()
{
foreach (array_keys($this->session->all()) as $key) {
if (0 === strpos($key, $this->namespace.'/')) {
$this->session->remove($key);
}
}
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Logout;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class CsrfTokenClearingLogoutHandler implements LogoutHandlerInterface
{
private $csrfTokenStorage;
public function __construct(ClearableTokenStorageInterface $csrfTokenStorage)
{
$this->csrfTokenStorage = $csrfTokenStorage;
}
public function logout(Request $request, Response $response, TokenInterface $token)
{
$this->csrfTokenStorage->clear();
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Tests\Logout;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
use Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler;
class CsrfTokenClearingLogoutHandlerTest extends TestCase
{
private $session;
private $csrfTokenStorage;
private $csrfTokenClearingLogoutHandler;
protected function setUp()
{
$this->session = new Session(new MockArraySessionStorage());
$this->csrfTokenStorage = new SessionTokenStorage($this->session, 'foo');
$this->csrfTokenStorage->setToken('foo', 'bar');
$this->csrfTokenStorage->setToken('foobar', 'baz');
$this->csrfTokenClearingLogoutHandler = new CsrfTokenClearingLogoutHandler($this->csrfTokenStorage);
}
public function testCsrfTokenCookieWithSameNamespaceIsRemoved()
{
$this->assertSame('bar', $this->session->get('foo/foo'));
$this->assertSame('baz', $this->session->get('foo/foobar'));
$this->csrfTokenClearingLogoutHandler->logout(new Request(), new Response(), $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock());
$this->assertFalse($this->csrfTokenStorage->hasToken('foo'));
$this->assertFalse($this->csrfTokenStorage->hasToken('foobar'));
$this->assertFalse($this->session->has('foo/foo'));
$this->assertFalse($this->session->has('foo/foobar'));
}
public function testCsrfTokenCookieWithDifferentNamespaceIsNotRemoved()
{
$barNamespaceCsrfSessionStorage = new SessionTokenStorage($this->session, 'bar');
$barNamespaceCsrfSessionStorage->setToken('foo', 'bar');
$barNamespaceCsrfSessionStorage->setToken('foobar', 'baz');
$this->assertSame('bar', $this->session->get('foo/foo'));
$this->assertSame('baz', $this->session->get('foo/foobar'));
$this->assertSame('bar', $this->session->get('bar/foo'));
$this->assertSame('baz', $this->session->get('bar/foobar'));
$this->csrfTokenClearingLogoutHandler->logout(new Request(), new Response(), $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock());
$this->assertTrue($barNamespaceCsrfSessionStorage->hasToken('foo'));
$this->assertTrue($barNamespaceCsrfSessionStorage->hasToken('foobar'));
$this->assertSame('bar', $barNamespaceCsrfSessionStorage->getToken('foo'));
$this->assertSame('baz', $barNamespaceCsrfSessionStorage->getToken('foobar'));
$this->assertFalse($this->csrfTokenStorage->hasToken('foo'));
$this->assertFalse($this->csrfTokenStorage->hasToken('foobar'));
$this->assertFalse($this->session->has('foo/foo'));
$this->assertFalse($this->session->has('foo/foobar'));
$this->assertSame('bar', $this->session->get('bar/foo'));
$this->assertSame('baz', $this->session->get('bar/foobar'));
}
}

View File

@ -25,9 +25,12 @@
},
"require-dev": {
"symfony/routing": "~3.4|~4.0",
"symfony/security-csrf": "~3.4|~4.0",
"symfony/security-csrf": "^3.4.11|^4.0.11",
"psr/log": "~1.0"
},
"conflict": {
"symfony/security-csrf": ">=3.4.0,<3.4.11 || >=4.0.0,<4.0.11"
},
"suggest": {
"symfony/security-csrf": "For using tokens to protect authentication/logout attempts",
"symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs"