8000 [Security] Allow LogoutListener to validate CSRF tokens · symfony/symfony@aaaa040 · GitHub
[go: up one dir, main page]

Skip to content

Commit aaaa040

Browse files
committed
[Security] Allow LogoutListener to validate CSRF tokens
This adds several new options to the logout listener, modeled after the form_login listener: * csrf_parameter * intention * csrf_provider The "csrf_parameter" and "intention" have default values if omitted. By default, "csrf_provider" is empty and CSRF validation is disabled in LogoutListener (preserving BC). If a service ID is given for "csrf_provider", CSRF validation will be enabled. Invalid tokens will result in an InvalidCsrfTokenException being thrown before any logout handlers are invoked.
1 parent b1f545b commit aaaa040

File tree

4 files changed

+90
-19
lines changed

4 files changed

+90
-19
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
200200
->treatTrueLike(array())
201201
->canBeUnset()
202202
->children()
203+
->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end()
204+
->scalarNode('csrf_provider')->cannotBeEmpty()->end()
205+
->scalarNode('intention')->defaultValue('logout')->end()
203206
->scalarNode('path')->defaultValue('/logout')->end()
204207
->scalarNode('target')->defaultValue('/')->end()
205208
->scalarNode('success_handler')->end()

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,10 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
277277
$listenerId = 'security.logout_listener.'.$id;
278278
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener'));
279279
$listener->replaceArgument(2, array(
280-
'logout_path' => $firewall['logout']['path'],
281-
'target_url' => $firewall['logout']['target'],
280+
'csrf_parameter' => $firewall['logout']['csrf_parameter'],
281+
'intention' => $firewall['logout']['intention'],
282+
'logout_path' => $firewall['logout']['path'],
283+
'target_url' => $firewall['logout']['target'],
282284
));
283285
$listeners[] = new Reference($listenerId);
284286

@@ -287,6 +289,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
287289
$listener->replaceArgument(4, new Reference($firewall['logout']['success_handler']));
288290
}
289291

292+
// add CSRF provider
293+
if (isset($firewall['logout']['csrf_provider'])) {
294+
$listener->addArgument(new Reference($firewall['logout']['csrf_provider']));
295+
}
296+
290297
// add session logout handler
291298
if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
292299
$listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));

src/Symfony/Component/Security/Http/Firewall/LogoutListener.php

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
namespace Symfony\Component\Security\Http\Firewall;
1313

14-
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
15-
16-
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
17-
use Symfony\Component\Security\Core\SecurityContextInterface;
18-
use Symfony\Component\Security\Http\HttpUtils;
14+
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
1915
use Symfony\Component\HttpFoundation\Request;
2016
use Symfony\Component\HttpFoundation\Response;
2117
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
18+
use Symfony\Component\Security\Core\SecurityContextInterface;
19+
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
20+
use Symfony\Component\Security\Http\HttpUtils;
21+
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
22+
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
2223

2324
/**
2425
* LogoutListener logout users.
@@ -32,24 +33,29 @@ class LogoutListener implements ListenerInterface
3233
private $handlers;
3334
private $successHandler;
3435
private $httpUtils;
36+
private $csrfProvider;
3537

3638
/**
3739
* Constructor
3840
*
3941
* @param SecurityContextInterface $securityContext
4042
* @param HttpUtils $httpUtils An HttpUtilsInterface instance
41-
* @param array $options An array of options for the processing of a logout attempt
42-
* @param LogoutSuccessHandlerInterface $successHandler
43+
* @param array $options An array of options to process a logout attempt
44+
* @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance
45+
* @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance
4346
*/
44-
public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, array $options = array(), LogoutSuccessHandlerInterface $successHandler = null)
47+
public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, array $options = array(), LogoutSuccessHandlerInterface $successHandler = null, CsrfProviderInterface $csrfProvider = null)
4548
{
4649
$this->securityContext = $securityContext;
4750
$this->httpUtils = $httpUtils;
4851
$this->options = array_merge(array(
49-
'logout_path' => '/logout',
50-
'target_url' => '/',
52+
'csrf_parameter' => '_csrf_token',
53+
'intention' => 'logout',
54+
'logout_path' => '/logout',
55+
'target_url' => '/',
5156
), $options);
5257
$this->successHandler = $successHandler;
58+
$this->csrfProvider = $csrfProvider;
5359
$this->handlers = array();
5460
}
5561

@@ -66,7 +72,12 @@ public function addHandler(LogoutHandlerInterface $handler)
6672
/**
6773
* Performs the logout if requested
6874
*
75+
* If a CsrfProviderInterface instance is available, it will be used to
76+
* validate the request.
77+
*
6978
* @param GetResponseEvent $event A GetResponseEvent instance
79+
* @throws InvalidCsrfTokenException if the CSRF token is invalid
80+
* @throws RuntimeException if the LogoutSuccessHandlerInterface instance does not return a response
7081
*/
7182
public function handle(GetResponseEvent $event)
7283
{
@@ -76,6 +87,14 @@ public function handle(GetResponseEvent $event)
7687
return;
7788
}
7889

90+
if (null !== $this->csrfProvider) {
91+
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
92+
93+
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
94+
throw new InvalidCsrfTokenException('Invalid CSRF token.');
95+
}
96+
}
97+
7998
if (null !== $this->successHandler) {
8099
$response = $this->successHandler->onLogoutSuccess($request);
81100

tests/Symfony/Tests/Component/Security/Http/Firewall/LogoutListenerTest.php

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,27 @@ public function testHandleUnmatchedPath()
3434
$listener->handle($event);
3535
}
3636

37-
public function testHandleMatchedPathWithSuccessHandler()
37+
public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation()
3838
{
3939
$successHandler = $this->getSuccessHandler();
40+
$csrfProvider = $this->getCsrfProvider();
4041

41-
list($listener, $context, $httpUtils, $options) = $this->getListener($successHandler);
42+
list($listener, $context, $httpUtils, $options) = $this->getListener($successHandler, $csrfProvider);
4243

4344
list($event, $request) = $this->getGetResponseEvent();
4445

46+
$request->query->set('_csrf_token', $csrfToken = 'token');
47+
4548
$httpUtils->expects($this->once())
4649
->method('checkRequestPath')
4750
->with($request, $options['logout_path'])
4851
->will($this->returnValue(true));
4952

53+
$csrfProvider->expects($this->once())
54+
->method('isCsrfTokenValid')
55+
->with('logout', $csrfToken)
56+
->will($this->returnValue(true));
57+
5058
$successHandler->expects($this->once())
5159
->method('onLogoutSuccess')
5260
->with($request)
@@ -74,7 +82,7 @@ public function testHandleMatchedPathWithSuccessHandler()
7482
$listener->handle($event);
7583
}
7684

77-
public function testHandleMatchedPathWithoutSuccessHandler()
85+
public function testHandleMatchedPathWithoutSuccessHandlerAndCsrfValidation()
7886
{
7987
list($listener, $context, $httpUtils, $options) = $this->getListener();
8088

@@ -136,6 +144,37 @@ public function testSuccessHandlerReturnsNonResponse()
136144
$listener->handle($event);
137145
}
138146

147+
/**
148+
* @expectedException Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException
149+
*/
150+
public function testCsrfValidationFails()
151+
{
152+
$csrfProvider = $this->getCsrfProvider();
153+
154+
list($listener, $context, $httpUtils, $options) = $this->getListener(null, $csrfProvider);
155+
156+
list($event, $request) = $this->getGetResponseEvent();
157+
158+
$request->query->set('_csrf_token', $csrfToken = 'token');
159+
160+
$httpUtils->expects($this->once())
161+
->method('checkRequestPath')
162+
->with($request, $options['logout_path'])
163+
->will($this->returnValue(true));
164+
165+
$csrfProvider->expects($this->once())
166+
->method('isCsrfTokenValid')
167+
->with('logout', $csrfToken)
168+
->will($this->returnValue(false));
169+
170+
$listener->handle($event);
171+
}
172+
173+
private function getCsrfProvider()
174+
{
175+
return $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
176+
}
177+
139178
private function getContext()
140179
{
141180
return $this->getMockBuilder('Symfony\Component\Security\Core\SecurityContext')
@@ -168,16 +207,19 @@ private function getHttpUtils()
168207
->getMock();
169208
}
170209

171-
private function getListener($successHandler = null)
210+
private function getListener($successHandler = null, $csrfProvider = null)
172211
{
173212
$listener = new LogoutListener(
174213
$context = $this->getContext(),
175214
$httpUtils = $this->getHttpUtils(),
176215
$options = array(
177-
'logout_path' => '/logout',
178-
'target_url' => '/',
216+
'csrf_parameter' => '_csrf_token',
217+
'intention' => 'logout',
218+
'logout_path' => '/logout',
219+
'target_url' => '/',
179220
),
180-
$successHandler
221+
$successHandler,
222+
$csrfProvider
181223
);
182224

183225
return array($listener, $context, $httpUtils, $options);

0 commit comments

Comments
 (0)
0