@@ -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 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+
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+ stateless CSRF tokens, see 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).
@@ -302,6 +310,160 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an
302310attacker from guessing the CSRF tokens, a random mask is prepended to the token
303311and used to scramble it.
304312
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+
305466 .. _`Cross-site request forgery` : https://en.wikipedia.org/wiki/Cross-site_request_forgery
306467.. _`BREACH` : https://en.wikipedia.org/wiki/BREACH
307468.. _`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