@@ -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).
@@ -221,15 +229,15 @@ generate a CSRF token in the template and store it as a hidden form field:
221
229
.. code-block :: html+twig
222
230
223
231
<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
224
- {# the argument of csrf_token() is an arbitrary string used to generate the token #}
232
+ {# the argument of csrf_token() is the ID of this token #}
225
233
<input type="hidden" name="token" value="{{ csrf_token('delete-item') }}">
226
234
227
235
<button type="submit">Delete item</button>
228
236
</form>
229
237
230
238
Then, get the value of the CSRF token in the controller action and use the
231
239
:method: `Symfony\\ Bundle\\ FrameworkBundle\\ Controller\\ AbstractController::isCsrfTokenValid `
232
- method to check its validity::
240
+ method to check its validity, passing the same token ID used in the template ::
233
241
234
242
use Symfony\Component\HttpFoundation\Request;
235
243
use Symfony\Component\HttpFoundation\Response;
@@ -302,6 +310,166 @@ 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
+ Using a Default Token ID
378
+ ~~~~~~~~~~~~~~~~~~~~~~~~
379
+
380
+ While stateful CSRF tokens are better seggregated per form or action, stateless
381
+ ones don't need many token identifiers. In the previous example, ``authenticate ``
382
+ and ``logout `` are listed because they're the default identifiers used by the
383
+ Symfony Security component. The ``submit `` identifier is then listed so that
384
+ form types defined by the application can use it by default. The following
385
+ configuration - which applies only to form types declared using autofiguration
386
+ (the default way to declare *your * services) - will make your form types use the
387
+ ``submit `` token identifier by default:
388
+
389
+ .. configuration-block ::
390
+
391
+ .. code-block :: yaml
392
+
393
+ # config/packages/csrf.yaml
394
+ framework :
395
+ form :
396
+ csrf_protection :
397
+ token_id : ' submit'
398
+
399
+ .. code-block :: xml
400
+
401
+ <!-- config/packages/csrf.xml -->
402
+ <?xml version =" 1.0" encoding =" UTF-8" ?>
403
+ <container xmlns =" http://symfony.com/schema/dic/services"
404
+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
405
+ xmlns : framework =" http://symfony.com/schema/dic/symfony"
406
+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
407
+ https://symfony.com/schema/dic/services/services-1.0.xsd
408
+ http://symfony.com/schema/dic/symfony
409
+ https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" >
410
+
411
+ <framework : config >
412
+ <framework : form >
413
+ <framework : csrf-protection token-id =" submit" />
414
+ </framework : form >
415
+ </framework : config >
416
+ </container >
417
+
418
+ .. code-block :: php
419
+
420
+ // config/packages/csrf.php
421
+ use Symfony\Config\FrameworkConfig;
422
+
423
+ return static function (FrameworkConfig $framework): void {
424
+ $framework->form()
425
+ ->csrfProtection()
426
+ ->tokenId('submit')
427
+ ;
428
+ };
429
+
430
+ Forms configured with a token identifier listed in the above ``stateless_token_ids ``
431
+ option will use the stateless CSRF protection.
432
+
433
+ Generating CSRF Token Using Javascript
434
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
435
+
436
+ In addition to the ``Origin `` and ``Referer `` headers, stateless CSRF protection
437
+ also checks a cookie and a header (named ``csrf-token `` by default, see the
438
+ :ref: `CSRF configuration reference <reference-framework-csrf-protection >`).
439
+
440
+ These extra checks are part of defense-in-depth strategies provided by the
441
+ stateless CSRF protection. They are optional and they require
442
+ `some JavaScript `_ to be activated. This JavaScript is responsible for generating
443
+ a crypto-safe random token when a form is submitted, then putting the token in
444
+ the hidden CSRF field of the form and submitting it also as a cookie and header.
445
+ On the server-side, the CSRF token is validated by checking the cookie and header
446
+ values. This "double-submit" protection relies on the same-origin policy
447
+ implemented by browsers and is strengthened by regenerating the token at every
448
+ form submission - which prevents cookie fixation issues - and by using
449
+ ``samesite=strict `` and ``__Host- `` cookies, which make them domain-bound and
450
+ HTTPS-only.
451
+
452
+ Note that the default snippet of JavaScript provided by Symfony requires that
453
+ the hidden CSRF form field is either named ``_csrf_token ``, or that it has the
454
+ ``data-controller="csrf-protection" `` attribute. You can of course take
455
+ inspiration from this snippet to write your own, provided you follow the same
456
+ protocol.
457
+
458
+ As a last measure, a behavioral check is added on the server-side to ensure that
459
+ the validation method cannot be downgraded: if and only if a session is already
460
+ available, successful "double-submit" is remembered and is then required for
461
+ subsequent requests. This prevents attackers from exploiting potentially reduced
462
+ validation checks once cookie and/or header validation has been confirmed as
463
+ effective (they're optional by default as explained above).
464
+
465
+ .. note ::
466
+
467
+ Enforcing successful "double-submit" for every requests is not recommended as
468
+ as it could lead to a broken user experience. The opportunistic approach
469
+ described above is preferred because it allows the application to gracefully
470
+ degrade to ``Origin `` / ``Referer `` checks when JavaScript is not available.
471
+
305
472
.. _`Cross-site request forgery` : https://en.wikipedia.org/wiki/Cross-site_request_forgery
306
473
.. _`BREACH` : https://en.wikipedia.org/wiki/BREACH
307
474
.. _`CRIME` : https://en.wikipedia.org/wiki/CRIME
475
+ .. _`some JavaScript` : https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js
0 commit comments