Skip to content

Commit 91e5b65

Browse files
[Security] Implement stateless headers/cookies-based CSRF protection
1 parent 9a6fd2d commit 91e5b65

File tree

8 files changed

+84
-8
lines changed

8 files changed

+84
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ CHANGELOG
1414
* Deprecate making `cache.app` adapter taggable, use the `cache.app.taggable` adapter instead
1515
* Enable `json_decode_detailed_errors` in the default serializer context in debug mode by default when `seld/jsonlint` is installed
1616
* Register `Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter` as a service named `serializer.name_converter.snake_case_to_camel_case` if available
17+
* Add `framework.csrf_protection.stateless_token_ids`, `.cookie_name`, and `.check_header` options to use stateless headers/cookies-based CSRF protection
18+
* Add `framework.form.csrf_protection.field_attr` option
1719
* Deprecate `session.sid_length` and `session.sid_bits_per_character` config options
1820
* Add the ability to use an existing service as a lock/semaphore resource
1921
* Add support for configuring multiple serializer instances via the configuration

DependencyInjection/Configuration.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,22 @@ private function addCsrfSection(ArrayNodeDefinition $rootNode): void
209209
->treatTrueLike(['enabled' => true])
210210
->treatNullLike(['enabled' => true])
211211
->addDefaultsIfNotSet()
212+
->fixXmlConfig('stateless_token_id')
212213
->children()
213-
// defaults to framework.session.enabled && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class)
214-
->booleanNode('enabled')->defaultNull()->end()
214+
// defaults to framework.csrf_protection.stateless_token_ids || framework.session.enabled && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class)
215+
->scalarNode('enabled')->defaultNull()->end()
216+
->arrayNode('stateless_token_ids')
217+
->scalarPrototype()->end()
218+
->info('Enable headers/cookies-based CSRF validation for the listed token ids.')
219+
->end()
220+
->scalarNode('check_header')
221+
->defaultFalse()
222+
->info('Whether to check the CSRF token in a header in addition to a cookie when using stateless protection.')
223+
->end()
224+
->scalarNode('cookie_name')
225+
->defaultValue('csrf-token')
226+
->info('The name of the cookie to use when using stateless protection.')
227+
->end()
215228
->end()
216229
->end()
217230
->end()
@@ -232,8 +245,14 @@ private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableI
232245
->treatNullLike(['enabled' => true])
233246
->addDefaultsIfNotSet()
234247
->children()
235-
->booleanNode('enabled')->defaultNull()->end() // defaults to framework.csrf_protection.enabled
248+
->scalarNode('enabled')->defaultNull()->end() // defaults to framework.csrf_protection.enabled
249+
->scalarNode('token_id')->defaultNull()->end()
236250
->scalarNode('field_name')->defaultValue('_token')->end()
251+
->arrayNode('field_attr')
252+
->performNoDeepMerging()
253+
->scalarPrototype()->end()
254+
->defaultValue(['data-controller' => 'csrf-protection'])
255+
->end()
237256
->end()
238257
->end()
239258
->end()

DependencyInjection/FrameworkExtension.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ public function load(array $configs, ContainerBuilder $container): void
464464

465465
// csrf depends on session being registered
466466
if (null === $config['csrf_protection']['enabled']) {
467-
$this->writeConfigEnabled('csrf_protection', $this->readConfigEnabled('session', $container, $config['session']) && !class_exists(FullStack::class) && ContainerBuilder::willBeAvailable('symfony/security-csrf', CsrfTokenManagerInterface::class, ['symfony/framework-bundle']), $config['csrf_protection']);
467+
$this->writeConfigEnabled('csrf_protection', $config['csrf_protection']['stateless_token_ids'] || $this->readConfigEnabled('session', $container, $config['session']) && !class_exists(FullStack::class) && ContainerBuilder::willBeAvailable('symfony/security-csrf', CsrfTokenManagerInterface::class, ['symfony/framework-bundle']), $config['csrf_protection']);
468468
}
469469
$this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader);
470470

@@ -765,6 +765,10 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
765765

766766
$container->setParameter('form.type_extension.csrf.enabled', true);
767767
$container->setParameter('form.type_extension.csrf.field_name', $config['form']['csrf_protection']['field_name']);
768+
$container->setParameter('form.type_extension.csrf.field_attr', $config['form']['csrf_protection']['field_attr']);
769+
770+
$container->getDefinition('form.type_extension.csrf')
771+
->replaceArgument(7, $config['form']['csrf_protection']['token_id']);
768772
} else {
769773
$container->setParameter('form.type_extension.csrf.enabled', false);
770774
}
@@ -1815,8 +1819,7 @@ private function registerSecurityCsrfConfiguration(array $config, ContainerBuild
18151819
if (!class_exists(\Symfony\Component\Security\Csrf\CsrfToken::class)) {
18161820
throw new LogicException('CSRF support cannot be enabled as the Security CSRF component is not installed. Try running "composer require symfony/security-csrf".');
18171821
}
1818-
1819-
if (!$this->isInitializedConfigEnabled('session')) {
1822+
if (!$config['stateless_token_ids'] && !$this->isInitializedConfigEnabled('session')) {
18201823
throw new \LogicException('CSRF protection needs sessions to be enabled.');
18211824
}
18221825

@@ -1826,6 +1829,24 @@ private function registerSecurityCsrfConfiguration(array $config, ContainerBuild
18261829
if (!class_exists(CsrfExtension::class)) {
18271830
$container->removeDefinition('twig.extension.security_csrf');
18281831
}
1832+
1833+
if (!$config['stateless_token_ids']) {
1834+
$container->removeDefinition('security.csrf.same_origin_token_manager');
1835+
1836+
return;
1837+
}
1838+
1839+
$container->getDefinition('security.csrf.same_origin_token_manager')
1840+
->replaceArgument(3, $config['stateless_token_ids'])
1841+
->replaceArgument(4, $config['check_header'])
1842+
->replaceArgument(5, $config['cookie_name']);
1843+
1844+
if (!$this->isInitializedConfigEnabled('session')) {
1845+
$container->setAlias('security.csrf.token_manager', 'security.csrf.same_origin_token_manager');
1846+
$container->getDefinition('security.csrf.same_origin_token_manager')
1847+
->setDecoratedService(null)
1848+
->replaceArgument(2, null);
1849+
}
18291850
}
18301851

18311852
private function registerSerializerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void

Resources/config/form_csrf.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
service('translator')->nullOnInvalid(),
2424
param('validator.translation_domain'),
2525
service('form.server_params'),
26+
param('form.type_extension.csrf.field_attr'),
27+
abstract_arg('framework.form.csrf_protection.token_id'),
2628
])
2729
->tag('form.type_extension')
2830
;

Resources/config/schema/symfony-1.0.xsd

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,25 @@
7171
</xsd:complexType>
7272

7373
<xsd:complexType name="form_csrf_protection">
74+
<xsd:sequence>
75+
<xsd:element name="field-attr" type="field_attr" minOccurs="0" maxOccurs="unbounded" />
76+
</xsd:sequence>
7477
<xsd:attribute name="enabled" type="xsd:boolean" />
78+
<xsd:attribute name="token-id" type="xsd:string" />
7579
<xsd:attribute name="field-name" type="xsd:string" />
7680
</xsd:complexType>
7781

82+
<xsd:complexType name="field_attr">
83+
<xsd:attribute name="name" type="xsd:string" use="required"/>
84+
</xsd:complexType>
85+
7886
<xsd:complexType name="csrf_protection">
87+
<xsd:sequence>
88+
<xsd:element name="stateless-token-id" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
89+
</xsd:sequence>
7990
<xsd:attribute name="enabled" type="xsd:boolean" />
91+
<xsd:attribute name="check-header" type="xsd:string" />
92+
<xsd:attribute name="cookie-name" type="xsd:string" />
8093
</xsd:complexType>
8194

8295
<xsd:complexType name="esi">

Resources/config/security_csrf.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Bridge\Twig\Extension\CsrfRuntime;
1616
use Symfony\Component\Security\Csrf\CsrfTokenManager;
1717
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
18+
use Symfony\Component\Security\Csrf\SameOriginCsrfTokenManager;
1819
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
1920
use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator;
2021
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
@@ -46,5 +47,18 @@
4647

4748
->set('twig.extension.security_csrf', CsrfExtension::class)
4849
->tag('twig.extension')
50+
51+
->set('security.csrf.same_origin_token_manager', SameOriginCsrfTokenManager::class)
52+
->decorate('security.csrf.token_manager')
53+
->args([
54+
service('request_stack'),
55+
service('logger')->nullOnInvalid(),
56+
service('.inner'),
57+
abstract_arg('framework.csrf_protection.stateless_token_ids'),
58+
abstract_arg('framework.csrf_protection.check_header'),
59+
abstract_arg('framework.csrf_protection.cookie_name'),
60+
])
61+
->tag('monolog.logger', ['channel' => 'request'])
62+
->tag('kernel.event_listener', ['event' => 'kernel.response', 'method' => 'onKernelResponse'])
4963
;
5064
};

Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,13 +715,18 @@ protected static function getBundleDefaultConfig()
715715
'trusted_proxies' => ['%env(default::SYMFONY_TRUSTED_PROXIES)%'],
716716
'trusted_headers' => ['%env(default::SYMFONY_TRUSTED_HEADERS)%'],
717717
'csrf_protection' => [
718-
'enabled' => false,
718+
'enabled' => null,
719+
'cookie_name' => 'csrf-token',
720+
'check_header' => false,
721+
'stateless_token_ids' => [],
719722
],
720723
'form' => [
721724
'enabled' => !class_exists(FullStack::class),
722725
'csrf_protection' => [
723726
'enabled' => null, // defaults to csrf_protection.enabled
724727
'field_name' => '_token',
728+
'field_attr' => ['data-controller' => 'csrf-protection'],
729+
'token_id' => null,
725730
],
726731
],
727732
'esi' => ['enabled' => false],

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
"symfony/runtime": "<6.4.13|>=7.0,<7.1.6",
9797
"symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4",
9898
"symfony/serializer": "<6.4",
99-
"symfony/security-csrf": "<6.4",
99+
"symfony/security-csrf": "<7.2",
100100
"symfony/security-core": "<6.4",
101101
"symfony/stopwatch": "<6.4",
102102
"symfony/translation": "<6.4",

0 commit comments

Comments
 (0)