Skip to content

Commit

Permalink
feature #58095 [Security] Implement stateless headers/cookies-based C…
Browse files Browse the repository at this point in the history
…SRF protection (nicolas-grekas)

This PR was merged into the 7.2 branch.

Discussion
----------

[Security] Implement stateless headers/cookies-based CSRF protection

| Q             | A
| ------------- | ---
| Branch?       | 7.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | #13464
| License       | MIT

 #54705 made me think about our CSRF protection and I wrote the attached CSRF token manager to implement stateless headers/cookies-based validation.

By defaults, the existing stateful manager is used.
In order to leverage this new stateless manager, one needs to list the token ids that should be managed this way:

```yaml
framework:
    csrf_protection:
        stateless_token_ids: [my_stateless_token_id]
```

 * This CSRF token manager uses a combination of cookie and headers to validate non-persistent tokens.
 *
 * This manager is designed to be stateless and compatible with HTTP-caching.
 *
 * First, we validate the source of the request using the Origin/Referer headers. This relies
 * on the app being able to know its own target origin. Don't miss configuring your reverse proxy to
 * send the X-Forwarded-* / Forwarded headers if you're behind one.
 *
 * Then, we validate the request using a cookie and a CsrfToken. If the cookie is found, it should
 * contain the same value as the CsrfToken. A JavaScript snippet on the client side is responsible
 * for performing this double-submission. The token value should be regenerated on every request
 * using a cryptographically secure random generator.
 *
 * If either double-submit or Origin/Referer headers are missing, it typically indicates that
 * JavaScript is disabled on the client side, or that the JavaScript snippet was not properly
 * implemented, or that the Origin/Referer headers were filtered out.
 *
 * Requests lacking both double-submit and origin information are deemed insecure.
 *
 * When a session is found, a behavioral check is added to ensure that the validation method does not
 * downgrade from double-submit to origin checks. This prevents attackers from exploiting potentially
 * less secure validation methods once a more secure method has been confirmed as functional.
 *
 * On HTTPS connections, the cookie is prefixed with "__Host-" to prevent it from being forged on an
 * HTTP channel. On the JS side, the cookie should be set with samesite=strict to strengthen the CSRF
 * protection. The cookie is always cleared on the response to prevent any further use of the token.
 *
 * The $checkHeader argument allows the token to be checked in a header instead of or in addition to a
 * cookie. This makes it harder for an attacker to forge a request, though it may also pose challenges
 * when setting the header depending on the client-side framework in use.
 *
 * When a fallback CSRF token manager is provided, only tokens listed in the $tokenIds argument will be
 * managed by this manager. All other tokens will be delegated to the fallback manager.
```

Since it's stateless, end users won't loose their content if they take time to submit a form: even if the session is destroyed while they populate their form, remember-me will reconnect them and the form will be accepted.

Recipe update at symfony/recipes#1337

Commits
-------

27d8a31d105 [Security] Implement stateless headers/cookies-based CSRF protection
  • Loading branch information
nicolas-grekas committed Oct 8, 2024
2 parents 64116e0 + 6a49eed commit bcf5a68
Showing 1 changed file with 16 additions and 1 deletion.
17 changes: 16 additions & 1 deletion Extension/Csrf/Type/FormTypeCsrfExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
Expand All @@ -35,6 +36,8 @@ public function __construct(
private ?TranslatorInterface $translator = null,
private ?string $translationDomain = null,
private ?ServerParams $serverParams = null,
private array $fieldAttr = [],
private ?string $defaultTokenId = null,
) {
}

Expand Down Expand Up @@ -73,6 +76,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
$csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [
'block_prefix' => 'csrf_token',
'mapped' => false,
'attr' => $this->fieldAttr + ['autocomplete' => 'off'],
]);

$view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
Expand All @@ -81,13 +85,24 @@ public function finishView(FormView $view, FormInterface $form, array $options):

public function configureOptions(OptionsResolver $resolver): void
{
if ($defaultTokenId = $this->defaultTokenId) {
$defaultTokenManager = $this->defaultTokenManager;
$defaultTokenId = static fn (Options $options) => $options['csrf_token_manager'] === $defaultTokenManager ? $defaultTokenId : null;
}

$resolver->setDefaults([
'csrf_protection' => $this->defaultEnabled,
'csrf_field_name' => $this->defaultFieldName,
'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
'csrf_token_manager' => $this->defaultTokenManager,
'csrf_token_id' => null,
'csrf_token_id' => $defaultTokenId,
]);

$resolver->setAllowedTypes('csrf_protection', 'bool');
$resolver->setAllowedTypes('csrf_field_name', 'string');
$resolver->setAllowedTypes('csrf_message', 'string');
$resolver->setAllowedTypes('csrf_token_manager', CsrfTokenManagerInterface::class);
$resolver->setAllowedTypes('csrf_token_id', ['null', 'string']);
}

public static function getExtendedTypes(): iterable
Expand Down

0 comments on commit bcf5a68

Please sign in to comment.