@@ -34,6 +34,17 @@ unique tokens added to forms as hidden fields. The legit server validates them t
34
34
ensure that the request originated from the expected source and not some other
35
35
malicious website.
36
36
37
+ Anti-CSRF tokens can be managed either in a stateful way: they're put in the
38
+ session and are unique for each user and for each kind of action, or in a
39
+ stateless way: they're generated on the client-side, are unique for each
40
+ requests.
41
+
42
+ Symfony supports both stateful and stateless anti-CSRF tokens.
43
+
44
+ .. versionadded 7.3:
45
+
46
+ Stateless anti-CSRF protection was introduced in Symfony 7.3.
47
+
37
48
Installation
38
49
------------
39
50
@@ -85,14 +96,14 @@ for more information):
85
96
;
86
97
};
87
98
88
- The tokens used for CSRF protection are meant to be different for every user and
89
- they are stored in the session. That's why a session is started automatically as
90
- soon as you render a form with CSRF protection.
99
+ By default, the tokens used for CSRF protection are stored in the session.
100
+ That's why a session is started automatically as soon as you render a form
101
+ with CSRF protection.
91
102
92
103
.. _caching-pages-that-contain-csrf-protected-forms :
93
104
94
- Moreover, this means that you cannot fully cache pages that include CSRF
95
- protected forms. As an alternative, you can :
105
+ This leads to many strategies to help with caching pages that include CSRF
106
+ protected forms, among them :
96
107
97
108
* Embed the form inside an uncached :doc: `ESI fragment </http_cache/esi >` and
98
109
cache the rest of the page contents;
@@ -101,6 +112,9 @@ protected forms. As an alternative, you can:
101
112
load the CSRF token with an uncached AJAX request and replace the form
102
113
field value with it.
103
114
115
+ The most effective way to cache pages that need CSRF protected forms is to use
116
+ stateless CSRF tokens, see below.
117
+
104
118
.. _csrf-protection-forms :
105
119
106
120
CSRF Protection in Symfony Forms
@@ -183,14 +197,15 @@ method of each form::
183
197
'csrf_field_name' => '_token',
184
198
// an arbitrary string used to generate the value of the token
185
199
// using a different string for each form improves its security
200
+ // when using stateful tokens (which is the default)
186
201
'csrf_token_id' => 'task_item',
187
202
]);
188
203
}
189
204
190
205
// ...
191
206
}
192
207
193
- You can also customize the rendering of the CSRF form field creating a custom
208
+ You can also customize the rendering of the CSRF form field by creating a custom
194
209
:doc: `form theme </form/form_themes >` and using ``csrf_token `` as the prefix of
195
210
the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %} `` to
196
211
customize the entire form field contents).
@@ -305,3 +320,154 @@ and used to scramble it.
305
320
.. _`Cross-site request forgery` : https://en.wikipedia.org/wiki/Cross-site_request_forgery
306
321
.. _`BREACH` : https://en.wikipedia.org/wiki/BREACH
307
322
.. _`CRIME` : https://en.wikipedia.org/wiki/CRIME
323
+
324
+ Stateless CSRF Tokens
325
+ ---------------------
326
+
327
+ By default CSRF tokens are stateful, which means they're stored in the session.
328
+ But some token ids can be declared as stateless using the ``stateless_token_ids ``
329
+ option:
330
+
331
+ .. configuration-block ::
332
+
333
+ .. code-block :: yaml
334
+
335
+ # config/packages/framework.yaml
336
+ framework :
337
+ # ...
338
+ csrf_protection :
339
+ stateless_token_ids : ['submit', 'authenticate', 'logout']
340
+
341
+ .. code-block :: xml
342
+
343
+ <!-- config/packages/framework.xml -->
344
+ <?xml version =" 1.0" encoding =" UTF-8" ?>
345
+ <container xmlns =" http://symfony.com/schema/dic/services"
346
+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
347
+ xmlns : framework =" http://symfony.com/schema/dic/symfony"
348
+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
349
+ https://symfony.com/schema/dic/services/services-1.0.xsd
350
+ http://symfony.com/schema/dic/symfony
351
+ https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" >
352
+
353
+ <framework : config >
354
+ <framework : csrf-protection >
355
+ <framework : stateless-token-id >submit</framework : stateless-token-id >
356
+ <framework : stateless-token-id >authenticate</framework : stateless-token-id >
357
+ <framework : stateless-token-id >logout</framework : stateless-token-id >
358
+ </framework : csrf-protection >
359
+ </framework : config >
360
+ </container >
361
+
362
+ .. code-block :: php
363
+
364
+ // config/packages/framework.php
365
+ use Symfony\Config\FrameworkConfig;
366
+
367
+ return static function (FrameworkConfig $framework): void {
368
+ $framework->csrfProtection()
369
+ ->statelessTokenIds(['submit', 'authenticate', 'logout'])
370
+ ;
371
+ };
372
+
373
+ Stateless CSRF tokens use a CSRF protection that doesn't need the session. This
374
+ means that you can cache the entire page and still have CSRF protection.
375
+
376
+ When a stateless CSRF token is checked for validity, Symfony verifies the
377
+ ``Origin `` and the ``Referer `` headers of the incoming HTTP request.
378
+
379
+ If either of these headers match the target origin of the application (its domain
380
+ name), the CSRF token is considered valid. This relies on the app being able to
381
+ know its own target origin. Don't miss configuring your reverse proxy if you're
382
+ behind one. See :doc: `/deployment/proxies `.
383
+
384
+ While stateful CSRF tokens are better seggregated per form or action, stateless
385
+ ones don't need many token identifiers. In the previous example, ``authenticate ``
386
+ and ``logout `` are listed because they're the default identifiers used by the
387
+ Symfony security component. The ``submit `` identifier is then listed so that
388
+ form types defined by the application can use it by default. The following
389
+ configuration - which applies only to form types declared using autofiguration
390
+ (the default way to declare *your * services) - will make your form types use the
391
+ ``submit `` token identifier by default:
392
+
393
+ .. configuration-block ::
394
+
395
+ .. code-block :: yaml
396
+
397
+ # config/packages/csrf.yaml
398
+ framework :
399
+ form :
400
+ csrf_protection :
401
+ token_id : ' submit'
402
+
403
+ .. code-block :: xml
404
+
405
+ <!-- config/packages/csrf.xml -->
406
+ <?xml version =" 1.0" encoding =" UTF-8" ?>
407
+ <container xmlns =" http://symfony.com/schema/dic/services"
408
+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
409
+ xmlns : framework =" http://symfony.com/schema/dic/symfony"
410
+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
411
+ https://symfony.com/schema/dic/services/services-1.0.xsd
412
+ http://symfony.com/schema/dic/symfony
413
+ https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" >
414
+
415
+ <framework : config >
416
+ <framework : form >
417
+ <framework : csrf-protection token-id =" submit" />
418
+ </framework : form >
419
+ </framework : config >
420
+ </container >
421
+
422
+ .. code-block :: php
423
+
424
+ // config/packages/csrf.php
425
+ use Symfony\Config\FrameworkConfig;
426
+
427
+ return static function (FrameworkConfig $framework): void {
428
+ $framework->form()
429
+ ->csrfProtection()
430
+ ->tokenId('submit')
431
+ ;
432
+ };
433
+
434
+ Forms configured with a token identifier listed in the above ``stateless_token_ids ``
435
+ option will use the stateless CSRF protection.
436
+
437
+ In addition to the ``Origin `` and ``Referer `` headers, stateless CSRF protection
438
+ also checks a cookie and a header (named ``csrf-token `` by default, see the
439
+ :ref: `CSRF configuration reference <reference-framework-csrf-protection >`).
440
+
441
+ These extra checks are part of defense-in-depth strategies provided by the
442
+ stateless CSRF protection. They are optional and they require
443
+ `some JavaScript `_ to be activated. This JavaScript is responsible for generating
444
+ a crypto-safe random token when a form is submitted, then putting the token in
445
+ the hidden CSRF field of the form and submitting it also as a cookie and header.
446
+ On the server-side, the CSRF token is validated by checking the cookie and header
447
+ values. This "double-submit" protection relies on the same-origin policy
448
+ implemented by browsers and is strengthened by regenerating the token at every
449
+ form submission - which prevents cookie fixation issues - and by using
450
+ ``samesite=strict `` and ``__Host- `` cookies, which make them domain-bound and
451
+ HTTPS-only.
452
+
453
+ Note that the default snippet of JavaScript provided by Symfony requires that
454
+ the hidden CSRF form field is either named ``_csrf_token ``, or that it has the
455
+ ``data-controller="csrf-protection" `` attribute. You can of course take
456
+ inspiration from this snippet to write your own, provided you follow the same
457
+ protocol.
458
+
459
+ As a last measure, a behavioral check is added on the server-side to ensure that
460
+ the validation method cannot be downgraded: if and only if a session is already
461
+ available, successful "double-submit" is remembered and is then required for
462
+ subsequent requests. This prevents attackers from exploiting potentially reduced
463
+ validation checks once cookie and/or header validation has been confirmed as
464
+ effective (they're optional by default as explained above).
465
+
466
+ .. note:
467
+
468
+ Enforcing successful "double-submit" for every requests is not recommended as
469
+ as it could lead to a broken user experience. The opportunistic approach
470
+ described above is preferred because it allows the application to gracefully
471
+ degrade to ``Origin`` / ``Referer`` checks when JavaScript is not available.
472
+
473
+ _`some JavaScript`: https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js
0 commit comments