Skip to content

Commit 4579583

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

File tree

4 files changed

+233
-14
lines changed

4 files changed

+233
-14
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 & 4 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`` 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,15 +1198,32 @@ 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
1170-
..........
1205+
''''''''''
11711206

11721207
**type**: ``string`` **default**: ``_token``
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

Lines changed: 176 additions & 8 deletions
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).
@@ -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

230238
Then, 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;
@@ -302,6 +310,166 @@ 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+
Using a Default Token ID
378+
~~~~~~~~~~~~~~~~~~~~~~~~
379+
380+
While stateful CSRF tokens are better seggregated per form or action, stateless
381+
ones don't need many token identifiers. In the previous example, ``authenticate``
382+
and ``logout`` are listed because they're the default identifiers used by the
383+
Symfony Security component. The ``submit`` identifier is then listed so that
384+
form types defined by the application can use it by default. The following
385+
configuration - which applies only to form types declared using autofiguration
386+
(the default way to declare *your* services) - will make your form types use the
387+
``submit`` token identifier by default:
388+
389+
.. configuration-block::
390+
391+
.. code-block:: yaml
392+
393+
# config/packages/csrf.yaml
394+
framework:
395+
form:
396+
csrf_protection:
397+
token_id: 'submit'
398+
399+
.. code-block:: xml
400+
401+
<!-- config/packages/csrf.xml -->
402+
<?xml version="1.0" encoding="UTF-8" ?>
403+
<container xmlns="http://symfony.com/schema/dic/services"
404+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
405+
xmlns:framework="http://symfony.com/schema/dic/symfony"
406+
xsi:schemaLocation="http://symfony.com/schema/dic/services
407+
https://symfony.com/schema/dic/services/services-1.0.xsd
408+
http://symfony.com/schema/dic/symfony
409+
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
410+
411+
<framework:config>
412+
<framework:form>
413+
<framework:csrf-protection token-id="submit"/>
414+
</framework:form>
415+
</framework:config>
416+
</container>
417+
418+
.. code-block:: php
419+
420+
// config/packages/csrf.php
421+
use Symfony\Config\FrameworkConfig;
422+
423+
return static function (FrameworkConfig $framework): void {
424+
$framework->form()
425+
->csrfProtection()
426+
->tokenId('submit')
427+
;
428+
};
429+
430+
Forms configured with a token identifier listed in the above ``stateless_token_ids``
431+
option will use the stateless CSRF protection.
432+
433+
Generating CSRF Token Using Javascript
434+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
435+
436+
In addition to the ``Origin`` and ``Referer`` headers, stateless CSRF protection
437+
also checks a cookie and a header (named ``csrf-token`` by default, see the
438+
:ref:`CSRF configuration reference <reference-framework-csrf-protection>`).
439+
440+
These extra checks are part of defense-in-depth strategies provided by the
441+
stateless CSRF protection. They are optional and they require
442+
`some JavaScript`_ to be activated. This JavaScript is responsible for generating
443+
a crypto-safe random token when a form is submitted, then putting the token in
444+
the hidden CSRF field of the form and submitting it also as a cookie and header.
445+
On the server-side, the CSRF token is validated by checking the cookie and header
446+
values. This "double-submit" protection relies on the same-origin policy
447+
implemented by browsers and is strengthened by regenerating the token at every
448+
form submission - which prevents cookie fixation issues - and by using
449+
``samesite=strict`` and ``__Host-`` cookies, which make them domain-bound and
450+
HTTPS-only.
451+
452+
Note that the default snippet of JavaScript provided by Symfony requires that
453+
the hidden CSRF form field is either named ``_csrf_token``, or that it has the
454+
``data-controller="csrf-protection"`` attribute. You can of course take
455+
inspiration from this snippet to write your own, provided you follow the same
456+
protocol.
457+
458+
As a last measure, a behavioral check is added on the server-side to ensure that
459+
the validation method cannot be downgraded: if and only if a session is already
460+
available, successful "double-submit" is remembered and is then required for
461+
subsequent requests. This prevents attackers from exploiting potentially reduced
462+
validation checks once cookie and/or header validation has been confirmed as
463+
effective (they're optional by default as explained above).
464+
465+
.. note::
466+
467+
Enforcing successful "double-submit" for every requests is not recommended as
468+
as it could lead to a broken user experience. The opportunistic approach
469+
described above is preferred because it allows the application to gracefully
470+
degrade to ``Origin`` / ``Referer`` checks when JavaScript is not available.
471+
305472
.. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery
306473
.. _`BREACH`: https://en.wikipedia.org/wiki/BREACH
307474
.. _`CRIME`: https://en.wikipedia.org/wiki/CRIME
475+
.. _`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)