Skip to content

Navigation Menu

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 7c212b9

Browse filesBrowse files
[Security] Implement double-submit CSRF protection
1 parent f91514d commit 7c212b9
Copy full SHA for 7c212b9

File tree

Expand file treeCollapse file tree

14 files changed

+608
-11
lines changed
Filter options
Expand file treeCollapse file tree

14 files changed

+608
-11
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ CHANGELOG
1313
* Deprecate making `cache.app` adapter taggable, use the `cache.app.taggable` adapter instead
1414
* Enable `json_decode_detailed_errors` in the default serializer context in debug mode by default when `seld/jsonlint` is installed
1515
* Register `Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter` as a service named `serializer.name_converter.snake_case_to_camel_case` if available
16+
* Add `framework.csrf_protection.double_submit_token_ids`, `.cookie_name`, and `.check_header` options to use double-submit CSRF protection
17+
* Add `framework.form.csrf_protection.field_attr` option
1618
* Deprecate `session.sid_length` and `session.sid_bits_per_character` config options
1719

1820
7.1

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+22-3Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,22 @@ private function addCsrfSection(ArrayNodeDefinition $rootNode): void
212212
->treatTrueLike(['enabled' => true])
213213
->treatNullLike(['enabled' => true])
214214
->addDefaultsIfNotSet()
215+
->fixXmlConfig('double_submit_token_id')
215216
->children()
216-
// defaults to framework.session.enabled && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class)
217-
->booleanNode('enabled')->defaultNull()->end()
217+
// defaults to framework.csrf_protection.double_submit_token_ids || framework.session.enabled && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class)
218+
->scalarNode('enabled')->defaultNull()->end()
219+
->arrayNode('double_submit_token_ids')
220+
->scalarPrototype()->end()
221+
->info('Enable double-submit CSRF validation for the listed token ids.')
222+
->end()
223+
->scalarNode('check_header')
224+
->defaultFalse()
225+
->info('Whether to check the CSRF token in a header in addition to a cookie when using double-submission.')
226+
->end()
227+
->scalarNode('cookie_name')
228+
->defaultValue('csrf-token')
229+
->info('The name of the cookie to use when using double-submission.')
230+
->end()
218231
->end()
219232
->end()
220233
->end()
@@ -235,8 +248,14 @@ private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableI
235248
->treatNullLike(['enabled' => true])
236249
->addDefaultsIfNotSet()
237250
->children()
238-
->booleanNode('enabled')->defaultNull()->end() // defaults to framework.csrf_protection.enabled
251+
->scalarNode('enabled')->defaultNull()->end() // defaults to framework.csrf_protection.enabled
252+
->scalarNode('token_id')->defaultNull()->end()
239253
->scalarNode('field_name')->defaultValue('_token')->end()
254+
->arrayNode('field_attr')
255+
->performNoDeepMerging()
256+
->scalarPrototype()->end()
257+
->defaultValue(['data-controller' => 'csrf-protection'])
258+
->end()
240259
->end()
241260
->end()
242261
->end()

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+24-3Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ public function load(array $configs, ContainerBuilder $container): void
463463

464464
// csrf depends on session being registered
465465
if (null === $config['csrf_protection']['enabled']) {
466-
$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']);
466+
$this->writeConfigEnabled('csrf_protection', $config['csrf_protection']['double_submit_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']);
467467
}
468468
$this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader);
469469

@@ -764,6 +764,10 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
764764

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

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

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

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php
+2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+13Lines 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="double-submit-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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php
+14Lines 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\DoubleSubmitCsrfTokenManager;
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.double_submit_token_manager', DoubleSubmitCsrfTokenManager::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.double_submit_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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,18 @@ protected static function getBundleDefaultConfig()
718718
'x-forwarded-proto',
719719
],
720720
'csrf_protection' => [
721-
'enabled' => false,
721+
'enabled' => null,
722+
'cookie_name' => 'csrf-token',
723+
'check_header' => false,
724+
'double_submit_token_ids' => Array ()
722725
],
723726
'form' => [
724727
'enabled' => !class_exists(FullStack::class),
725728
'csrf_protection' => [
726729
'enabled' => null, // defaults to csrf_protection.enabled
727730
'field_name' => '_token',
731+
'field_attr' => ['data-controller' => 'csrf-protection'],
732+
'token_id' => null,
728733
],
729734
],
730735
'esi' => ['enabled' => false],

‎src/Symfony/Bundle/FrameworkBundle/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/composer.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"symfony/property-access": "<6.4",
9696
"symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4",
9797
"symfony/serializer": "<6.4",
98-
"symfony/security-csrf": "<6.4",
98+
"symfony/security-csrf": "<7.2",
9999
"symfony/security-core": "<6.4",
100100
"symfony/stopwatch": "<6.4",
101101
"symfony/translation": "<6.4",

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php
+16-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Csrf/CHANGELOG.md
+5Lines 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 `DoubleSubmitCsrfTokenManager`
8+
49
6.0
510
---
611

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.