Skip to content

Commit 6329b3d

Browse files
[Security] Tell about stateless CSRF protection
1 parent d90c725 commit 6329b3d

File tree

4 files changed

+224
-11
lines changed

4 files changed

+224
-11
lines changed

http_cache/varnish.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ If you know for sure that the backend never uses sessions or basic
6262
authentication, have Varnish remove the corresponding header from requests to
6363
prevent clients from bypassing the cache. In practice, you will need sessions
6464
at least for some parts of the site, e.g. when using forms with
65-
:doc:`CSRF Protection </security/csrf>`. In this situation, make sure to
65+
:doc:`stateful CSRF Protection </security/csrf>`. In this situation, make sure to
6666
:ref:`only start a session when actually needed <session-avoid-start>`
6767
and clear the session when it is no longer needed. Alternatively, you can look
6868
into :ref:`caching pages that contain CSRF protected forms <caching-pages-that-contain-csrf-protected-forms>`.

reference/configuration/framework.rst

+54-3
Original file line numberDiff line numberDiff line change
@@ -805,8 +805,6 @@ csrf_protection
805805

806806
For more information about CSRF protection, see :doc:`/security/csrf`.
807807

808-
.. _reference-csrf_protection-enabled:
809-
810808
enabled
811809
.......
812810

@@ -854,6 +852,42 @@ If you're using forms, but want to avoid starting your session (e.g. using
854852
forms in an API-only website), ``csrf_protection`` will need to be set to
855853
``false``.
856854

855+
stateless_token_ids
856+
...................
857+
858+
**type**: ``array`` **default**: ``[]``
859+
860+
The list of CSRF token ids that will use stateless CSRF protection.
861+
862+
.. versionadded:: 7.2
863+
864+
This option was added in Symfony 7.2 to aid in configuring stateless CSRF protection.
865+
866+
check_header
867+
............
868+
869+
**type**: ``integer`` or ``bool`` **default**: ``false``
870+
871+
Whether to check the CSRF token in a header in addition to a cookie when using stateless protection.
872+
Can be set to ``2`` (the value of the ``CHECK_ONLY_HEADER`` constant on the
873+
:class:`Symfony\\Component\\Security\\Csrf\\SameOriginCsrfTokenManager` class) to check only the header
874+
and not the cookie.
875+
876+
.. versionadded:: 7.2
877+
878+
This option was added in Symfony 7.2 to aid in configuring stateless CSRF protection.
879+
880+
cookie_name
881+
...........
882+
883+
**type**: ``string`` **default**: ``csrf-token``
884+
885+
The name of the cookie (and header) to use for the double-submit when using stateless protection.
886+
887+
.. versionadded:: 7.2
888+
889+
This option was added in Symfony 7.2 to aid in configuring stateless CSRF protection.
890+
857891
.. _config-framework-default_locale:
858892

859893
default_locale
@@ -1164,7 +1198,8 @@ settings is configured.
11641198

11651199
For more details, see :doc:`/forms`.
11661200

1167-
.. _reference-form-field-name:
1201+
csrf_protection
1202+
'''''''''''''''
11681203

11691204
field_name
11701205
..........
@@ -1173,6 +1208,22 @@ field_name
11731208

11741209
This is the field name that you should give to the CSRF token field of your forms.
11751210

1211+
field_attr
1212+
..........
1213+
1214+
**type**: ``array`` **default**: ``['data-controller' => 'csrf-protection']``
1215+
1216+
This is the HTML attributes that should be added to the CSRF token field of your forms.
1217+
1218+
token_id
1219+
........
1220+
1221+
**type**: ``string`` **default**: ``null``
1222+
1223+
This is the CSRF token id that should be used for validating the CSRF tokens of your forms.
1224+
Note that this setting applies only to autoconfigured form types, which usually means only
1225+
to your own form types and not to form types registered by third-party bundles.
1226+
11761227
fragments
11771228
~~~~~~~~~
11781229

security/csrf.rst

+168-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ unique tokens added to forms as hidden fields. The legit server validates them t
3434
ensure that the request originated from the expected source and not some other
3535
malicious 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+
3741
Installation
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

106113
CSRF 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
195203
the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %}`` to
196204
customize the entire form field contents).
@@ -302,6 +310,160 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an
302310
attacker from guessing the CSRF tokens, a random mask is prepended to the token
303311
and 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

session.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ sessions for anonymous users, you must *completely* avoid accessing the session.
115115
.. note::
116116

117117
Sessions will also be started when using features that rely on them internally,
118-
such as the :ref:`CSRF protection in forms <csrf-protection-forms>`.
118+
such as the :ref:`stateful CSRF protection in forms <csrf-protection-forms>`.
119119

120120
.. _flash-messages:
121121

0 commit comments

Comments
 (0)