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

Skip to content

Commit 27d8a31

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

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) && in 6D47 terface_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_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

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\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
};

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 `SameOriginCsrfTokenManager`
8+
49
6.0
510
---
611

0 commit comments

Comments
 (0)
0