Skip to content

Commit 91c2882

Browse files
committed
Merge branch '7.2' into 7.3
* 7.2: Rewords [Security] Tell about stateless CSRF protection
2 parents 844ec35 + e12256c commit 91c2882

File tree

4 files changed

+239
-14
lines changed

4 files changed

+239
-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: 57 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,44 @@ 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 :ref:`stateless CSRF protection <csrf-stateless-tokens>`.
861+
862+
.. versionadded:: 7.2
863+
864+
The ``stateless_token_ids`` option was introduced in Symfony 7.2.
865+
866+
check_header
867+
............
868+
869+
**type**: ``integer`` or ``bool`` **default**: ``false``
870+
871+
Whether to check the CSRF token in an HTTP header in addition to the cookie when
872+
using :ref:`stateless CSRF protection <csrf-stateless-tokens>`. You can also set
873+
this to ``2`` (the value of the ``CHECK_ONLY_HEADER`` constant on the
874+
:class:`Symfony\\Component\\Security\\Csrf\\SameOriginCsrfTokenManager` class)
875+
to check only the header and ignore the cookie.
876+
877+
.. versionadded:: 7.2
878+
879+
The ``check_header`` option was introduced in Symfony 7.2.
880+
881+
cookie_name
882+
...........
883+
884+
**type**: ``string`` **default**: ``csrf-token``
885+
886+
The name of the cookie (and HTTP header) to use for the double-submit when using
887+
:ref:`stateless CSRF protection <csrf-stateless-tokens>`.
888+
889+
.. versionadded:: 7.2
890+
891+
The ``cookie_name`` option was introduced in Symfony 7.2.
892+
857893
.. _config-framework-default_locale:
858894

859895
default_locale
@@ -1171,15 +1207,32 @@ settings is configured.
11711207

11721208
For more details, see :doc:`/forms`.
11731209

1174-
.. _reference-form-field-name:
1210+
csrf_protection
1211+
...............
11751212

11761213
field_name
1177-
..........
1214+
''''''''''
11781215

11791216
**type**: ``string`` **default**: ``_token``
11801217

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

1220+
field_attr
1221+
''''''''''
1222+
1223+
**type**: ``array`` **default**: ``['data-controller' => 'csrf-protection']``
1224+
1225+
HTML attributes to add to the CSRF token field of your forms.
1226+
1227+
token_id
1228+
''''''''
1229+
1230+
**type**: ``string`` **default**: ``null``
1231+
1232+
The CSRF token ID used to validate the CSRF tokens of your forms. This setting
1233+
applies only to form types that use :ref:`service autoconfiguration <services-autoconfigure>`,
1234+
which typically means your own form types, not those registered by third-party bundles.
1235+
11831236
fragments
11841237
~~~~~~~~~
11851238

security/csrf.rst

Lines changed: 180 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 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+
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+
:ref:`stateless CSRF tokens <csrf-stateless-tokens>`, as explained 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;
@@ -317,6 +325,170 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an
317325
attacker from guessing the CSRF tokens, a random mask is prepended to the token
318326
and 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

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)