@@ -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 in two ways: using a **stateful ** approach,
38
+ where tokens are stored in the session and are unique per user and action; or a
39
+ **stateless ** approach, where tokens are 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
+ :ref: `stateless CSRF tokens <csrf-stateless-tokens >`, as explained 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;
@@ -317,6 +325,170 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an
317
325
attacker from guessing the CSRF tokens, a random mask is prepended to the token
318
326
and used to scramble it.
319
327
328
+ .. _csrf-stateless-tokens :
329
+
330
+ Stateless CSRF Tokens
331
+ ---------------------
332
+
333
+ .. versionadded :: 7.2
334
+
335
+ Stateless anti-CSRF protection was introduced in Symfony 7.2.
336
+
337
+ By default CSRF tokens are stateful, which means they're stored in the session.
338
+ But some token ids can be declared as stateless using the ``stateless_token_ids ``
339
+ option:
340
+
341
+ .. configuration-block ::
342
+
343
+ .. code-block :: yaml
344
+
345
+ # config/packages/csrf.yaml
346
+ framework :
347
+ # ...
348
+ csrf_protection :
349
+ stateless_token_ids : ['submit', 'authenticate', 'logout']
350
+
351
+ .. code-block :: xml
352
+
353
+ <!-- config/packages/csrf.xml -->
354
+ <?xml version =" 1.0" encoding =" UTF-8" ?>
355
+ <container xmlns =" http://symfony.com/schema/dic/services"
356
+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
357
+ xmlns : framework =" http://symfony.com/schema/dic/symfony"
358
+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
359
+ https://symfony.com/schema/dic/services/services-1.0.xsd
360
+ http://symfony.com/schema/dic/symfony
361
+ https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" >
362
+
363
+ <framework : config >
364
+ <framework : csrf-protection >
365
+ <framework : stateless-token-id >submit</framework : stateless-token-id >
366
+ <framework : stateless-token-id >authenticate</framework : stateless-token-id >
367
+ <framework : stateless-token-id >logout</framework : stateless-token-id >
368
+ </framework : csrf-protection >
369
+ </framework : config >
370
+ </container >
371
+
372
+ .. code-block :: php
373
+
374
+ // config/packages/csrf.php
375
+ use Symfony\Config\FrameworkConfig;
376
+
377
+ return static function (FrameworkConfig $framework): void {
378
+ $framework->csrfProtection()
379
+ ->statelessTokenIds(['submit', 'authenticate', 'logout'])
380
+ ;
381
+ };
382
+
383
+ Stateless CSRF tokens provide protection without relying on the session. This
384
+ allows you to fully cache pages while still protecting against CSRF attacks.
385
+
386
+ When validating a stateless CSRF token, Symfony checks the ``Origin `` and
387
+ ``Referer `` headers of the incoming HTTP request. If either header matches the
388
+ application's target origin (i.e. its domain), the token is considered valid.
389
+
390
+ This mechanism relies on the application being able to determine its own origin.
391
+ If you're behind a reverse proxy, make sure it's properly configured. See
392
+ :doc: `/deployment/proxies `.
393
+
394
+ Using a Default Token ID
395
+ ~~~~~~~~~~~~~~~~~~~~~~~~
396
+
397
+ Stateful CSRF tokens are typically scoped per form or action, while stateless
398
+ tokens don't require many identifiers.
399
+
400
+ In the example above, the ``authenticate `` and ``logout `` identifiers are listed
401
+ because they are used by default in the Symfony Security component. The ``submit ``
402
+ identifier is included so that form types defined by the application can also use
403
+ CSRF protection by default.
404
+
405
+ The following configuration applies only to form types registered via
406
+ :ref: `autoconfiguration <services-autoconfigure >` (which is the default for your
407
+ own services), and it sets ``submit `` as their default token identifier:
408
+
409
+ .. configuration-block ::
410
+
411
+ .. code-block :: yaml
412
+
413
+ # config/packages/csrf.yaml
414
+ framework :
415
+ form :
416
+ csrf_protection :
417
+ token_id : ' submit'
418
+
419
+ .. code-block :: xml
420
+
421
+ <!-- config/packages/csrf.xml -->
422
+ <?xml version =" 1.0" encoding =" UTF-8" ?>
423
+ <container xmlns =" http://symfony.com/schema/dic/services"
424
+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
425
+ xmlns : framework =" http://symfony.com/schema/dic/symfony"
426
+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
427
+ https://symfony.com/schema/dic/services/services-1.0.xsd
428
+ http://symfony.com/schema/dic/symfony
429
+ https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" >
430
+
431
+ <framework : config >
432
+ <framework : form >
433
+ <framework : csrf-protection token-id =" submit" />
434
+ </framework : form >
435
+ </framework : config >
436
+ </container >
437
+
438
+ .. code-block :: php
439
+
440
+ // config/packages/csrf.php
441
+ use Symfony\Config\FrameworkConfig;
442
+
443
+ return static function (FrameworkConfig $framework): void {
444
+ $framework->form()
445
+ ->csrfProtection()
446
+ ->tokenId('submit')
447
+ ;
448
+ };
449
+
450
+ Forms configured with a token identifier listed in the above ``stateless_token_ids ``
451
+ option will use the stateless CSRF protection.
452
+
453
+ Generating CSRF Token Using Javascript
454
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
455
+
456
+ In addition to the ``Origin `` and ``Referer `` HTTP headers, stateless CSRF protection
457
+ can also validate tokens using a cookie and a header (named ``csrf-token `` by
458
+ default; see the :ref: `CSRF configuration reference <reference-framework-csrf-protection >`).
459
+
460
+ These additional checks are part of the **defense-in-depth ** strategy provided by
461
+ stateless CSRF protection. They are optional and require `some JavaScript `_ to
462
+ be enabled. This JavaScript generates a cryptographically secure random token
463
+ when a form is submitted. It then inserts the token into the form's hidden CSRF
464
+ field and sends it in both a cookie and a request header.
465
+
466
+ On the server side, CSRF token validation compares the values in the cookie and
467
+ the header. This "double-submit" protection relies on the browser's same-origin
468
+ policy and is further hardened by:
469
+
470
+ * generating a new token for each submission (to prevent cookie fixation);
471
+ * using ``samesite=strict `` and ``__Host- `` cookie attributes (to enforce HTTPS
472
+ and limit the cookie to the current domain).
473
+
474
+ By default, the Symfony JavaScript snippet expects the hidden CSRF field to be
475
+ named ``_csrf_token `` or to include the ``data-controller="csrf-protection" ``
476
+ attribute. You can adapt this logic to your needs as long as the same protocol
477
+ is followed.
478
+
479
+ To prevent validation from being downgraded, an extra behavioral check is performed:
480
+ if (and only if) a session already exists, successful "double-submit" is remembered
481
+ and becomes required for future requests. This ensures that once the optional cookie/header
482
+ validation has been proven effective, it remains enforced for that session.
483
+
484
+ .. note ::
485
+
486
+ Enforcing "double-submit" validation on all requests is not recommended,
487
+ as it may lead to a broken user experience. The opportunistic approach
488
+ described above is preferred, allowing the application to gracefully
489
+ fall back to ``Origin `` / ``Referer `` checks when JavaScript is unavailable.
490
+
320
491
.. _`Cross-site request forgery` : https://en.wikipedia.org/wiki/Cross-site_request_forgery
321
492
.. _`BREACH` : https://en.wikipedia.org/wiki/BREACH
322
493
.. _`CRIME` : https://en.wikipedia.org/wiki/CRIME
494
+ .. _`some JavaScript` : https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js
0 commit comments