8000 [Security] Implement stateless headers/cookies-based CSRF protection · symfony/symfony@ecc326f · GitHub
[go: up one dir, main page]

Skip to content
Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ecc326f

Browse files
[Security] Implement stateless headers/cookies-based CSRF protection
1 parent 6864dbe commit ecc326f

File tree

13 files changed

+608
-10
lines changed

13 files changed

+608
-10
lines changed

src/Symfony/Bundle/FrameworkBundle/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

src/Symfony/Bundle/FrameworkBundle/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()

src/Symfony/Bundle/FrameworkBundle/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_site_token_manager');
1835+
1836+
return;
1837+
}
1838+
1839+
$container->getDefinition('security.csrf.same_site_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_site_token_manager');
1846+
$container->getDefinition('security.csrf.same_site_token_manager')
1847+
->setDecoratedService(null)
1848+
->replaceArgument(2, null);
1849+
}
18291850
}
18301851

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

src/Symfony/Bundle/FrameworkBundle/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
;

src/Symfony/Bundle/FrameworkBundle/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">

src/Symfony/Bundle/FrameworkBundle/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\SameSiteCsrfTokenManager;
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_site_token_manager', SameSiteCsrfTokenManager::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.same_site_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
};

src/Symfony/Bundle/FrameworkBundle/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],

src/Symfony/Bundle/FrameworkBundle/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",

src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Form\FormInterface;
2020
use Symfony\Component\Form\FormView;
2121
use Symfony\Component\Form\Util\ServerParams;
22+
use Symfony\Component\OptionsResolver\Options;
2223
use Symfony\Component\OptionsResolver\OptionsResolver;
2324
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
2425
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,6 +36,8 @@ public function __construct(
3536
private ?TranslatorInterface $translator = null,
3637
private ?string $translationDomain = null,
3738
private ?ServerParams $serverParams = null,
39+
private array $fieldAttr = [],
40+
private ?string $defaultTokenId = null,
3841
) {
3942
}
4043

@@ -73,6 +76,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
7376
$csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [
7477
'block_prefix' => 'csrf_token',
7578
'mapped' => false,
79+
'attr' => $this->fieldAttr + ['autocomplete' => 'off'],
7680
]);
7781

7882
$view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
@@ -81,13 +85,24 @@ public function finishView(FormView $view, FormInterface $form, array $options):
8185

8286
public function configureOptions(OptionsResolver $resolver): void
8387
{
88+
if ($defaultTokenId = $this->defaultTokenId) {
89+
$defaultTokenManager = $this->defaultTokenManager;
90+
$defaultTokenId = static fn (Options $options) => $options['csrf_token_manager'] === $defaultTokenManager ? $defaultTokenId : null;
91+
}
92+
8493
$resolver->setDefaults([
8594
'csrf_protection' => $this->defaultEnabled,
8695
'csrf_field_name' => $this->defaultFieldName,
8796
'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
8897
'csrf_token_manager' => $this->defaultTokenManager,
89-
'csrf_token_id' => null,
98+
'csrf_token_id' => $defaultTokenId,
9099
]);
100+
101+
$resolver->setAllowedTypes('csrf_protection', 'bool');
102+
$resolver->setAllowedTypes('csrf_field_name', 'string');
103+
$resolver->setAllowedTypes('csrf_message', 'string');
104+
$resolver->setAllowedTypes('csrf_token_manager', CsrfTokenManagerInterface::class);
105+
$resolver->setAllowedTypes('csrf_token_id', ['null', 'string']);
91106
}
92107

93108
public static function getExtendedTypes(): iterable

src/Symfony/Component/Security/Csrf/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add `SameSiteCsrfTokenManager`
8+
49
6.0
510
---
611

0 commit comments

Comments
 (0)
0