Skip to content

Commit 25d5287

Browse files
[Security] Tell about stateless CSRF protection
1 parent d90c725 commit 25d5287

File tree

4 files changed

+229
-10
lines changed

4 files changed

+229
-10
lines changed

http_cache/varnish.rst

Lines changed: 1 addition & 1 deletion
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

Lines changed: 55 additions & 2 deletions
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|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
@@ -1166,13 +1200,32 @@ settings is configured.
11661200

11671201
.. _reference-form-field-name:
11681202

1203+
csrf_protection
1204+
'''''''''''''''
1205+
11691206
field_name
11701207
..........
11711208

11721209
**type**: ``string`` **default**: ``_token``
11731210

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

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

security/csrf.rst

Lines changed: 172 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ 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, are unique for each
40+
requests.
41+
42+
Symfony supports both stateful and stateless anti-CSRF tokens.
43+
44+
.. versionadded 7.3:
45+
46+
Stateless anti-CSRF protection was introduced in Symfony 7.3.
47+
3748
Installation
3849
------------
3950

@@ -85,14 +96,14 @@ for more information):
8596
;
8697
};
8798
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.
99+
By default, the tokens used for CSRF protection are stored in the session.
100+
That's why a session is started automatically as soon as you render a form
101+
with CSRF protection.
91102

92103
.. _caching-pages-that-contain-csrf-protected-forms:
93104

94-
Moreover, this means that you cannot fully cache pages that include CSRF
95-
protected forms. As an alternative, you can:
105+
This leads to many strategies to help with caching pages that include CSRF
106+
protected forms, among them:
96107

97108
* Embed the form inside an uncached :doc:`ESI fragment </http_cache/esi>` and
98109
cache the rest of the page contents;
@@ -101,6 +112,9 @@ protected forms. As an alternative, you can:
101112
load the CSRF token with an uncached AJAX request and replace the form
102113
field value with it.
103114

115+
The most effective way to cache pages that need CSRF protected forms is to use
116+
stateless CSRF tokens, see below.
117+
104118
.. _csrf-protection-forms:
105119

106120
CSRF Protection in Symfony Forms
@@ -183,14 +197,15 @@ method of each form::
183197
'csrf_field_name' => '_token',
184198
// an arbitrary string used to generate the value of the token
185199
// using a different string for each form improves its security
200+
// when using stateful tokens (which is the default)
186201
'csrf_token_id' => 'task_item',
187202
]);
188203
}
189204

190205
// ...
191206
}
192207

193-
You can also customize the rendering of the CSRF form field creating a custom
208+
You can also customize the rendering of the CSRF form field by creating a custom
194209
:doc:`form theme </form/form_themes>` and using ``csrf_token`` as the prefix of
195210
the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %}`` to
196211
customize the entire form field contents).
@@ -305,3 +320,154 @@ and used to scramble it.
305320
.. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery
306321
.. _`BREACH`: https://en.wikipedia.org/wiki/BREACH
307322
.. _`CRIME`: https://en.wikipedia.org/wiki/CRIME
323+
324+
Stateless CSRF Tokens
325+
---------------------
326+
327+
By default CSRF tokens are stateful, which means they're stored in the session.
328+
But some token ids can be declared as stateless using the ``stateless_token_ids``
329+
option:
330+
331+
.. configuration-block::
332+
333+
.. code-block:: yaml
334+
335+
# config/packages/framework.yaml
336+
framework:
337+
# ...
338+
csrf_protection:
339+
stateless_token_ids: ['submit', 'authenticate', 'logout']
340+
341+
.. code-block:: xml
342+
343+
<!-- config/packages/framework.xml -->
344+
<?xml version="1.0" encoding="UTF-8" ?>
345+
<container xmlns="http://symfony.com/schema/dic/services"
346+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
347+
xmlns:framework="http://symfony.com/schema/dic/symfony"
348+
xsi:schemaLocation="http://symfony.com/schema/dic/services
349+
https://symfony.com/schema/dic/services/services-1.0.xsd
350+
http://symfony.com/schema/dic/symfony
351+
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
352+
353+
<framework:config>
354+
<framework:csrf-protection>
355+
<framework:stateless-token-id>submit</framework:stateless-token-id>
356+
<framework:stateless-token-id>authenticate</framework:stateless-token-id>
357+
<framework:stateless-token-id>logout</framework:stateless-token-id>
358+
</framework:csrf-protection>
359+
</framework:config>
360+
</container>
361+
362+
.. code-block:: php
363+
364+
// config/packages/framework.php
365+
use Symfony\Config\FrameworkConfig;
366+
367+
return static function (FrameworkConfig $framework): void {
368+
$framework->csrfProtection()
369+
->statelessTokenIds(['submit', 'authenticate', 'logout'])
370+
;
371+
};
372+
373+
Stateless CSRF tokens use a CSRF protection that doesn't need the session. This
374+
means that you can cache the entire page and still have CSRF protection.
375+
376+
When a stateless CSRF token is checked for validity, Symfony verifies the
377+
``Origin`` and the ``Referer`` headers of the incoming HTTP request.
378+
379+
If either of these headers match the target origin of the application (its domain
380+
name), the CSRF token is considered valid. This relies on the app being able to
381+
know its own target origin. Don't miss configuring your reverse proxy if you're
382+
behind one. See :doc:`/deployment/proxies`.
383+
384+
While stateful CSRF tokens are better seggregated per form or action, stateless
385+
ones don't need many token identifiers. In the previous example, ``authenticate``
386+
and ``logout`` are listed because they're the default identifiers used by the
387+
Symfony security component. The ``submit`` identifier is then listed so that
388+
form types defined by the application can use it by default. The following
389+
configuration - which applies only to form types declared using autofiguration
390+
(the default way to declare *your* services) - will make your form types use the
391+
``submit`` token identifier by default:
392+
393+
.. configuration-block::
394+
395+
.. code-block:: yaml
396+
397+
# config/packages/csrf.yaml
398+
framework:
399+
form:
400+
csrf_protection:
401+
token_id: 'submit'
402+
403+
.. code-block:: xml
404+
405+
<!-- config/packages/csrf.xml -->
406+
<?xml version="1.0" encoding="UTF-8" ?>
407+
<container xmlns="http://symfony.com/schema/dic/services"
408+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
409+
xmlns:framework="http://symfony.com/schema/dic/symfony"
410+
xsi:schemaLocation="http://symfony.com/schema/dic/services
411+
https://symfony.com/schema/dic/services/services-1.0.xsd
412+
http://symfony.com/schema/dic/symfony
413+
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
414+
415+
<framework:config>
416+
<framework:form>
417+
<framework:csrf-protection token-id="submit"/>
418+
</framework:form>
419+
</framework:config>
420+
</container>
421+
422+
.. code-block:: php
423+
424+
// config/packages/csrf.php
425+
use Symfony\Config\FrameworkConfig;
426+
427+
return static function (FrameworkConfig $framework): void {
428+
$framework->form()
429+
->csrfProtection()
430+
->tokenId('submit')
431+
;
432+
};
433+
434+
Forms configured with a token identifier listed in the above ``stateless_token_ids``
435+
option will use the stateless CSRF protection.
436+
437+
In addition to the ``Origin`` and ``Referer`` headers, stateless CSRF protection
438+
also checks a cookie and a header (named ``csrf-token`` by default, see the
439+
:ref:`CSRF configuration reference <reference-framework-csrf-protection>`).
440+
441+
These extra checks are part of defense-in-depth strategies provided by the
442+
stateless CSRF protection. They are optional and they require
443+
`some JavaScript`_ to be activated. This JavaScript is responsible for generating
444+
a crypto-safe random token when a form is submitted, then putting the token in
445+
the hidden CSRF field of the form and submitting it also as a cookie and header.
446+
On the server-side, the CSRF token is validated by checking the cookie and header
447+
values. This "double-submit" protection relies on the same-origin policy
448+
implemented by browsers and is strengthened by regenerating the token at every
449+
form submission - which prevents cookie fixation issues - and by using
450+
``samesite=strict`` and ``__Host-`` cookies, which make them domain-bound and
451+
HTTPS-only.
452+
453+
Note that the default snippet of JavaScript provided by Symfony requires that
454+
the hidden CSRF form field is either named ``_csrf_token``, or that it has the
455+
``data-controller="csrf-protection"`` attribute. You can of course take
456+
inspiration from this snippet to write your own, provided you follow the same
457+
protocol.
458+
459+
As a last measure, a behavioral check is added on the server-side to ensure that
460+
the validation method cannot be downgraded: if and only if a session is already
461+
available, successful "double-submit" is remembered and is then required for
462+
subsequent requests. This prevents attackers from exploiting potentially reduced
463+
validation checks once cookie and/or header validation has been confirmed as
464+
effective (they're optional by default as explained above).
465+
466+
.. note:
467+
468+
Enforcing successful "double-submit" for every requests is not recommended as
469+
as it could lead to a broken user experience. The opportunistic approach
470+
described above is preferred because it allows the application to gracefully
471+
degrade to ``Origin`` / ``Referer`` checks when JavaScript is not available.
472+
473+
_`some JavaScript`: https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js

session.rst

Lines changed: 1 addition & 1 deletion
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)