Skip to content

Commit f41a184

Browse files
committed
Add CSRF protection
1 parent f576173 commit f41a184

File tree

9 files changed

+192
-88
lines changed

9 files changed

+192
-88
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
455455
false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null,
456456
])
457457
;
458+
459+
$config->replaceArgument(12, $firewall['logout']);
458460
}
459461

460462
// Determine default entry point

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
'security.firewall.map' => service('security.firewall.map'),
8888
'security.user_checker' => service('security.user_checker'),
8989
'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'),
90+
'security.csrf.token_manager' => service('security.csrf.token_manager')->ignoreOnInvalid(),
9091
]),
9192
abstract_arg('authenticators'),
9293
])
@@ -207,6 +208,7 @@
207208
null,
208209
[], // listeners
209210
null, // switch_user
211+
null, // logout
210212
])
211213

212214
->set('security.logout_url_generator', LogoutUrlGenerator::class)

src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,21 @@
1616
*/
1717
final class FirewallConfig
1818
{
19-
private string $name;
20-
private string $userChecker;
21-
private ?string $requestMatcher;
22-
private bool $securityEnabled;
23-
private bool $stateless;
24-
private ?string $provider;
25-
private ?string $context;
26-
private ?string $entryPoint;
27-
private ?string $accessDeniedHandler;
28-
private ?string $accessDeniedUrl;
29-
private array $authenticators;
30-
private ?array $switchUser;
31-
32-
public function __construct(string $name, string $userChecker, string $requestMatcher = null, bool $securityEnabled = true, bool $stateless = false, string $provider = null, string $context = null, string $entryPoint = null, string $accessDeniedHandler = null, string $accessDeniedUrl = null, array $authenticators = [], array $switchUser = null)
33-
{
34-
$this->name = $name;
35-
$this->userChecker = $userChecker;
36-
$this->requestMatcher = $requestMatcher;
37-
$this->securityEnabled = $securityEnabled;
38-
$this->stateless = $stateless;
39-
$this->provider = $provider;
40-
$this->context = $context;
41-
$this->entryPoint = $entryPoint;
42-
$this->accessDeniedHandler = $accessDeniedHandler;
43-
$this->accessDeniedUrl = $accessDeniedUrl;
44-
$this->authenticators = $authenticators;
45-
$this->switchUser = $switchUser;
19+
public function __construct(
20+
private readonly string $name,
21+
private readonly string $userChecker,
22+
private readonly ?string $requestMatcher = null,
23+
private readonly bool $securityEnabled = true,
24+
private readonly bool $stateless = false,
25+
private readonly ?string $provider = null,
26+
private readonly ?string $context = null,
27+
private readonly ?string $entryPoint = null,
28+
private readonly ?string $accessDeniedHandler = null,
29+
private readonly ?string $accessDeniedUrl = null,
30+
private readonly array $authenticators = [],
31+
private readonly ?array $switchUser = null,
32+
private readonly ?array $logout = null
33+
) {
4634
}
4735

4836
public function getName(): string
@@ -111,4 +99,9 @@ public function getSwitchUser(): ?array
11199
{
112100
return $this->switchUser;
113101
}
102+
103+
public function getLogout(): ?array
104+
{
105+
return $this->logout;
106+
}
114107
}

src/Symfony/Bundle/SecurityBundle/Security/Security.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\HttpFoundation\Request;
1616
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1718
use Symfony\Component\Security\Core\Exception\LogicException;
19+
use Symfony\Component\Security\Core\Exception\LogoutException;
1820
use Symfony\Component\Security\Core\Security as LegacySecurity;
1921
use Symfony\Component\Security\Core\User\UserInterface;
22+
use Symfony\Component\Security\Csrf\CsrfToken;
2023
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
2124
use Symfony\Component\Security\Http\Event\LogoutEvent;
25+
use Symfony\Component\Security\Http\ParameterBagUtils;
2226
use Symfony\Contracts\Service\ServiceProviderInterface;
2327

2428
/**
@@ -69,17 +73,30 @@ public function login(UserInterface $user, string $authenticatorName = null, str
6973
*/
7074
public function logout(): ?Response
7175
{
76+
/** @var TokenStorageInterface $tokenStorage */
77+
$tokenStorage = $this->container->get('security.token_storage');
78+
79+
if (!($token = $tokenStorage->getToken()) || !$token->getUser()) {
80+
throw new LogicException('Unable to logout as there is no logged-in user.');
81+
}
82+
7283
$request = $this->container->get('request_stack')->getMainRequest();
73-
$logoutEvent = new LogoutEvent($request, $this->container->get('security.token_storage')->getToken());
74-
$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request);
7584

76-
if (!$firewallConfig) {
77-
throw new LogicException('It is not possible to logout, as the request is not behind a firewall.');
85+
if (!$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request)) {
86+
throw new LogicException('Unable to logout as the request is not behind a firewall.');
7887
}
79-
$firewallName = $firewallConfig->getName();
8088

81-
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallName)->dispatch($logoutEvent);
82-
$this->container->get('security.token_storage')->setToken();
89+
if ($this->container->has('security.csrf.token_manager') && $logoutConfig = $firewallConfig->getLogout()) {
90+
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $logoutConfig['csrf_parameter']);
91+
if (!\is_string($csrfToken) || false === $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) {
92+
throw new LogoutException('Invalid CSRF token.');
93+
}
94+
}
95+
96+
$logoutEvent = new LogoutEvent($request, $token);
97+
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallConfig->getName())->dispatch($logoutEvent);
98+
99+
$tokenStorage->setToken();
83100

84101
return $logoutEvent->getResponse();
85102
}

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public function testFirewalls()
141141
'',
142142
[],
143143
null,
144+
null,
144145
],
145146
[
146147
'secure',
@@ -165,6 +166,14 @@ public function testFirewalls()
165166
'parameter' => '_switch_user',
166167
'role' => 'ROLE_ALLOWED_TO_SWITCH',
167168
],
169+
[
170+
'csrf_parameter' => '_csrf_token',
171+
'csrf_token_id' => 'logout',
172+
'path' => '/logout',
173+
'target' => '/',
174+
'invalidate_session' => true,
175+
'delete_cookies' => [],
176+
],
168177
],
169178
[
170179
'host',
@@ -181,6 +190,7 @@ public function testFirewalls()
181190
'http_basic',
182191
],
183192
null,
193+
null,
184194
],
185195
[
186196
'with_user_checker',
@@ -197,6 +207,7 @@ public function testFirewalls()
197207
'http_basic',
198208
],
199209
null,
210+
null,
200211
],
201212
], $configs);
202213

src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Security\Core\User\InMemoryUser;
2121
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
2222
use Symfony\Component\Security\Core\User\UserInterface;
23+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
2324

2425
class SecurityTest extends AbstractWebTestCase
2526
{
@@ -88,31 +89,36 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider()
8889
* @testWith ["json_login"]
8990
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
9091
*/
91-
public function testLoginWithBuiltInAuthenticator(string $authenticator)
92+
public function testLogin(string $authenticator)
9293
{
93-
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
94+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
9495
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
9596
$client->request('GET', '/welcome');
9697
$response = $client->getResponse();
9798

9899
$this->assertInstanceOf(JsonResponse::class, $response);
99100
$this->assertSame(200, $response->getStatusCode());
100101
$this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true));
102+
$this->assertSame('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier());
101103
}
102104

103-
/**
104-
* @testWith ["json_login"]
105-
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
106-
*/
107-
public function testLogout(string $authenticator)
105+
public function testLogout()
108106
{
109-
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
110-
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
111-
$client->request('GET', '/welcome');
112-
$this->assertEquals('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier());
107+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
108+
$client->request('GET', '/force-logout');
109+
$response = $client->getResponse();
110+
111+
$this->assertSame(200, $response->getStatusCode());
112+
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
113+
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
114+
}
113115

114-
$client->request('GET', '/auto-logout');
116+
public function testLogoutWithCsrf()
117+
{
118+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml']);
119+
$client->request('GET', '/force-logout');
115120
$response = $client->getResponse();
121+
116122
$this->assertSame(200, $response->getStatusCode());
117123
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
118124
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
@@ -245,12 +251,16 @@ public function welcome()
245251

246252
class LogoutController
247253
{
248-
public function __construct(private Security $security)
254+
public function __construct(private Security $security, private ?CsrfTokenManagerInterface $csrfTokenManager = null)
249255
{
250256
}
251257

252-
public function logout()
258+
public function logout(Request $request)
253259
{
260+
$this->security->login(new InMemoryUser('chalasr', '', ['ROLE_USER']), 'json_login', 'default');
261+
if ($this->csrfTokenManager) {
262+
$request->query->set('_csrf_token', (string) $this->csrfTokenManager->getToken('logout'));
263+
}
254264
$this->security->logout();
255265

256266
return new JsonResponse(['message' => 'Logout successful']);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
imports:
2+
- { resource: ./../config/framework.yml }
3+
4+
services:
5+
# alias the service so we can access it in the tests
6+
functional_test.security.helper:
7+
alias: security.helper
8+
public: true
9+
10+
functional.test.security.token_storage:
11+
alias: security.token_storage
12+
public: true
13+
14+
Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
15+
arguments: ['@security.helper']
16+
public: true
17+
18+
Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
19+
arguments: ['@security.helper', '@security.csrf.token_manager']
20+
public: true
21+
22+
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
23+
24+
security:
25+
enable_authenticator_manager: true
26+
providers:
27+
in_memory:
28+
memory:
29+
users: []
30+
31+
firewalls:
32+
default:
33+
json_login:
34+
username_path: user.login
35+
password_path: user.password
36+
logout:
37+
path: /regular-logout
38+
custom_authenticators:
39+
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
40+
41+
access_control:
42+
- { path: ^/foo, roles: PUBLIC_ACCESS }

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ welcome:
22
path: /welcome
33
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome
44

5-
logout:
6-
path: /auto-logout
5+
force-logout:
6+
path: /force-logout
77
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy