@@ -34,6 +34,10 @@ unique tokens added to forms as hidden fields. The legit server validates them t
3434ensure that the request originated from the expected source and not some other
3535malicious website.
3636
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+
3741Installation
3842------------
3943
@@ -85,14 +89,14 @@ for more information):
8589 ;
8690 };
8791
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.
9195
9296.. _caching-pages-that-contain-csrf-protected-forms :
9397
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 :
96100
97101* Embed the form inside an uncached :doc: `ESI fragment </http_cache/esi >` and
98102 cache the rest of the page contents;
@@ -101,6 +105,9 @@ protected forms. As an alternative, you can:
101105 load the CSRF token with an uncached AJAX request and replace the form
102106 field value with it.
103107
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+
104111.. _csrf-protection-forms :
105112
106113CSRF Protection in Symfony Forms
@@ -183,14 +190,15 @@ method of each form::
183190 'csrf_field_name' => '_token',
184191 // an arbitrary string used to generate the value of the token
185192 // using a different string for each form improves its security
193+ // when using stateful tokens (which is the default)
186194 'csrf_token_id' => 'task_item',
187195 ]);
188196 }
189197
190198 // ...
191199 }
192200
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
194202:doc: `form theme </form/form_themes >` and using ``csrf_token `` as the prefix of
195203the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %} `` to
196204customize the entire form field contents).
@@ -221,15 +229,15 @@ generate a CSRF token in the template and store it as a hidden form field:
221229.. code-block :: html+twig
222230
223231 <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 #}
225233 <input type="hidden" name="token" value="{{ csrf_token('delete-item') }}">
226234
227235 <button type="submit">Delete item</button>
228236 </form>
229237
230238Then, get the value of the CSRF token in the controller action and use the
231239: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 ::
233241
234242 use Symfony\Component\HttpFoundation\Request;
235243 use Symfony\Component\HttpFoundation\Response;
@@ -317,6 +325,170 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an
317325attacker from guessing the CSRF tokens, a random mask is prepended to the token
318326and used to scramble it.
319327
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+
320491.. _`Cross-site request forgery` : https://en.wikipedia.org/wiki/Cross-site_request_forgery
321492.. _`BREACH` : https://en.wikipedia.org/wiki/BREACH
322493.. _`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