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