From 4579583cb6304e707413f0d4d5abaa1454572231 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 13 May 2025 14:04:30 +0200 Subject: [PATCH] [Security] Tell about stateless CSRF protection --- http_cache/varnish.rst | 2 +- reference/configuration/framework.rst | 59 ++++++++- security/csrf.rst | 184 ++++++++++++++++++++++++-- session.rst | 2 +- 4 files changed, 233 insertions(+), 14 deletions(-) diff --git a/http_cache/varnish.rst b/http_cache/varnish.rst index a9bb668c100..6fcb7fd766b 100644 --- a/http_cache/varnish.rst +++ b/http_cache/varnish.rst @@ -62,7 +62,7 @@ If you know for sure that the backend never uses sessions or basic authentication, have Varnish remove the corresponding header from requests to prevent clients from bypassing the cache. In practice, you will need sessions at least for some parts of the site, e.g. when using forms with -:doc:`CSRF Protection `. In this situation, make sure to +:doc:`stateful CSRF Protection `. In this situation, make sure to :ref:`only start a session when actually needed ` and clear the session when it is no longer needed. Alternatively, you can look into :ref:`caching pages that contain CSRF protected forms `. diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 599cbb0ba04..8fec2678c8d 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -805,8 +805,6 @@ csrf_protection For more information about CSRF protection, see :doc:`/security/csrf`. -.. _reference-csrf_protection-enabled: - enabled ....... @@ -854,6 +852,42 @@ If you're using forms, but want to avoid starting your session (e.g. using forms in an API-only website), ``csrf_protection`` will need to be set to ``false``. +stateless_token_ids +................... + +**type**: ``array`` **default**: ``[]`` + +The list of CSRF token ids that will use stateless CSRF protection. + +.. versionadded:: 7.2 + + This option was added in Symfony 7.2 to aid in configuring stateless CSRF protection. + +check_header +............ + +**type**: ``integer`` or ``bool`` **default**: ``false`` + +Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. +Can be set to ``2`` (the value of the ``CHECK_ONLY_HEADER`` constant on the +:class:`Symfony\\Component\\Security\\Csrf\\SameOriginCsrfTokenManager` class) to check only the header +and not the cookie. + +.. versionadded:: 7.2 + + This option was added in Symfony 7.2 to aid in configuring stateless CSRF protection. + +cookie_name +........... + +**type**: ``string`` **default**: ``csrf-token`` + +The name of the cookie (and header) to use for the double-submit when using stateless protection. + +.. versionadded:: 7.2 + + This option was added in Symfony 7.2 to aid in configuring stateless CSRF protection. + .. _config-framework-default_locale: default_locale @@ -1164,15 +1198,32 @@ settings is configured. For more details, see :doc:`/forms`. -.. _reference-form-field-name: +csrf_protection +............... field_name -.......... +'''''''''' **type**: ``string`` **default**: ``_token`` This is the field name that you should give to the CSRF token field of your forms. +field_attr +'''''''''' + +**type**: ``array`` **default**: ``['data-controller' => 'csrf-protection']`` + +This is the HTML attributes that should be added to the CSRF token field of your forms. + +token_id +'''''''' + +**type**: ``string`` **default**: ``null`` + +This is the CSRF token id that should be used for validating the CSRF tokens of your forms. +Note that this setting applies only to autoconfigured form types, which usually means only +to your own form types and not to form types registered by third-party bundles. + fragments ~~~~~~~~~ diff --git a/security/csrf.rst b/security/csrf.rst index 772b503ec39..2e7369d170f 100644 --- a/security/csrf.rst +++ b/security/csrf.rst @@ -34,6 +34,10 @@ unique tokens added to forms as hidden fields. The legit server validates them t ensure that the request originated from the expected source and not some other malicious website. +Anti-CSRF tokens can be managed either in a stateful way: they're put in the +session and are unique for each user and for each kind of action, or in a +stateless way: they're generated on the client-side. + Installation ------------ @@ -85,14 +89,14 @@ for more information): ; }; -The tokens used for CSRF protection are meant to be different for every user and -they are stored in the session. That's why a session is started automatically as -soon as you render a form with CSRF protection. +By default, the tokens used for CSRF protection are stored in the session. +That's why a session is started automatically as soon as you render a form +with CSRF protection. .. _caching-pages-that-contain-csrf-protected-forms: -Moreover, this means that you cannot fully cache pages that include CSRF -protected forms. As an alternative, you can: +This leads to many strategies to help with caching pages that include CSRF +protected forms, among them: * Embed the form inside an uncached :doc:`ESI fragment ` and cache the rest of the page contents; @@ -101,6 +105,9 @@ protected forms. As an alternative, you can: load the CSRF token with an uncached AJAX request and replace the form field value with it. +The most effective way to cache pages that need CSRF protected forms is to use +stateless CSRF tokens, see below. + .. _csrf-protection-forms: CSRF Protection in Symfony Forms @@ -183,6 +190,7 @@ method of each form:: 'csrf_field_name' => '_token', // an arbitrary string used to generate the value of the token // using a different string for each form improves its security + // when using stateful tokens (which is the default) 'csrf_token_id' => 'task_item', ]); } @@ -190,7 +198,7 @@ method of each form:: // ... } -You can also customize the rendering of the CSRF form field creating a custom +You can also customize the rendering of the CSRF form field by creating a custom :doc:`form theme ` and using ``csrf_token`` as the prefix of the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %}`` to customize the entire form field contents). @@ -221,7 +229,7 @@ generate a CSRF token in the template and store it as a hidden form field: .. code-block:: html+twig
- {# the argument of csrf_token() is an arbitrary string used to generate the token #} + {# the argument of csrf_token() is the ID of this token #} @@ -229,7 +237,7 @@ generate a CSRF token in the template and store it as a hidden form field: Then, get the value of the CSRF token in the controller action and use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::isCsrfTokenValid` -method to check its validity:: +method to check its validity, passing the same token ID used in the template:: use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -302,6 +310,166 @@ targeted parts of the plaintext. To mitigate these attacks, and prevent an attacker from guessing the CSRF tokens, a random mask is prepended to the token and used to scramble it. +Stateless CSRF Tokens +--------------------- + +.. versionadded:: 7.2 + + Stateless anti-CSRF protection was introduced in Symfony 7.2. + +By default CSRF tokens are stateful, which means they're stored in the session. +But some token ids can be declared as stateless using the ``stateless_token_ids`` +option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/csrf.yaml + framework: + # ... + csrf_protection: + stateless_token_ids: ['submit', 'authenticate', 'logout'] + + .. code-block:: xml + + + + + + + + submit + authenticate + logout + + + + + .. code-block:: php + + // config/packages/csrf.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->csrfProtection() + ->statelessTokenIds(['submit', 'authenticate', 'logout']) + ; + }; + +Stateless CSRF tokens use a CSRF protection that doesn't need the session. This +means that you can cache the entire page and still have CSRF protection. + +When a stateless CSRF token is checked for validity, Symfony verifies the +``Origin`` and the ``Referer`` headers of the incoming HTTP request. + +If either of these headers match the target origin of the application (its domain +name), the CSRF token is considered valid. This relies on the app being able to +know its own target origin. Don't miss configuring your reverse proxy if you're +behind one. See :doc:`/deployment/proxies`. + +Using a Default Token ID +~~~~~~~~~~~~~~~~~~~~~~~~ + +While stateful CSRF tokens are better seggregated per form or action, stateless +ones don't need many token identifiers. In the previous example, ``authenticate`` +and ``logout`` are listed because they're the default identifiers used by the +Symfony Security component. The ``submit`` identifier is then listed so that +form types defined by the application can use it by default. The following +configuration - which applies only to form types declared using autofiguration +(the default way to declare *your* services) - will make your form types use the +``submit`` token identifier by default: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/csrf.yaml + framework: + form: + csrf_protection: + token_id: 'submit' + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/csrf.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->form() + ->csrfProtection() + ->tokenId('submit') + ; + }; + +Forms configured with a token identifier listed in the above ``stateless_token_ids`` +option will use the stateless CSRF protection. + +Generating CSRF Token Using Javascript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the ``Origin`` and ``Referer`` headers, stateless CSRF protection +also checks a cookie and a header (named ``csrf-token`` by default, see the +:ref:`CSRF configuration reference `). + +These extra checks are part of defense-in-depth strategies provided by the +stateless CSRF protection. They are optional and they require +`some JavaScript`_ to be activated. This JavaScript is responsible for generating +a crypto-safe random token when a form is submitted, then putting the token in +the hidden CSRF field of the form and submitting it also as a cookie and header. +On the server-side, the CSRF token is validated by checking the cookie and header +values. This "double-submit" protection relies on the same-origin policy +implemented by browsers and is strengthened by regenerating the token at every +form submission - which prevents cookie fixation issues - and by using +``samesite=strict`` and ``__Host-`` cookies, which make them domain-bound and +HTTPS-only. + +Note that the default snippet of JavaScript provided by Symfony requires that +the hidden CSRF form field is either named ``_csrf_token``, or that it has the +``data-controller="csrf-protection"`` attribute. You can of course take +inspiration from this snippet to write your own, provided you follow the same +protocol. + +As a last measure, a behavioral check is added on the server-side to ensure that +the validation method cannot be downgraded: if and only if a session is already +available, successful "double-submit" is remembered and is then required for +subsequent requests. This prevents attackers from exploiting potentially reduced +validation checks once cookie and/or header validation has been confirmed as +effective (they're optional by default as explained above). + +.. note:: + + Enforcing successful "double-submit" for every requests is not recommended as + as it could lead to a broken user experience. The opportunistic approach + described above is preferred because it allows the application to gracefully + degrade to ``Origin`` / ``Referer`` checks when JavaScript is not available. + .. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery .. _`BREACH`: https://en.wikipedia.org/wiki/BREACH .. _`CRIME`: https://en.wikipedia.org/wiki/CRIME +.. _`some JavaScript`: https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js diff --git a/session.rst b/session.rst index 0c5348ec9e6..4c14638fc68 100644 --- a/session.rst +++ b/session.rst @@ -115,7 +115,7 @@ sessions for anonymous users, you must *completely* avoid accessing the session. .. note:: Sessions will also be started when using features that rely on them internally, - such as the :ref:`CSRF protection in forms `. + such as the :ref:`stateful CSRF protection in forms `. .. _flash-messages: