Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 8, 2024

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Aug 26, 2024

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:

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

@smnandre
Copy link
Member

(cc @tucksaun )

@nicolas-grekas nicolas-grekas force-pushed the csrf-double-submit branch 5 times, most recently from 5b81f2f to 601b9ba Compare August 27, 2024 12:38
@nicolas-grekas nicolas-grekas force-pushed the csrf-double-submit branch 2 times, most recently from 0e8e0da to 42233a3 Compare August 27, 2024 12:55
@jderusse
Copy link
Member

AWASP discourage using this pattern and recommands using a hash/crypted value coupled to the session (or JWT lifetime) instead

https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#naive-double-submit-cookie-pattern-discouraged

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Aug 27, 2024

I've read that, and what I'm proposing here is significantly different from what they discourage to be considered seriously IMHO, esp with the new cookie attributes and with the style of double-submit I'm using here (which is not discussed on that page).

Aka I'd be interested in an open-minded threat model analysis of what this protects against (and what are the drawbacks)

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Aug 27, 2024

For additional background: the OWASP page links to https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf to give more insights about vulnerabilities with the "naive double-submit".

I read that doc too, and they don't apply to this PR:

  • MITM are ruled out by HTTPS nowadays (and no CSRF techniques can resist to MITM anyway)
  • HTTPS is enforced for cookies by the __Host- prefix, which prevents overriding them from HTTP
  • overriding the cookie isn't enough since the double-submit happens via a custom header (which is listed as a valid CSRF-protection on that PDF and on the OWASP page)
  • the Origin header is checked also when available

Binding CSRF tokens to any server-side state is ruling out statelessness/cacheability of the strategy so I'm not looking for anything hashed/signed/etc.

@nicolas-grekas nicolas-grekas force-pushed the csrf-double-submit branch 3 times, most recently from 09d6d78 to 66ae045 Compare August 28, 2024 09:45
@wouterj
Copy link
Member

wouterj commented Sep 17, 2024

Fyi: I'm planning to look at this on Thursday. Need some focus time to properly digest all information in here :)

I don't mind if this gets merged before it, there is enough time till feature freeze to tweak it in a follow up PR if necessary.

Copy link
Member

@wouterj wouterj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like it may be the case that all my comments make no sense whatsoever, I had quite the struggle following the logic. Please be patient with me 😉


One of the struggles is the name of the feature. If I'm correct, the stance of Symfony 7.2 would be: verify origin and do not use tokens for CSRF protection.
Then, for cases where origin headers might not be set (e.g. corporate environments), we fallback to CSRF token protection using a modified double-submit pattern.

If that is the case, I think we should not use "double-submit" as the main name of the new feature (it's just the fallback). Setting double_submit_token_ids to enable origin checks for some token IDs sounds wrong. Let's brainstorm a better name if you agree.

In addition, I'm not sure if we should default to all the complexity the double-submit brings if this is just a fallback. Do we want to push JS code to the users that they probably don't even need? Or should we somehow make the recipe optional and have some docs that says "run composer install csrf-double-submit-pack if you can't (always) use origin checks".


Finally, by generating the CSRF token and creating the cookie in the JS code moves a security-critical part of the double-submit flow to the application code. I'm always much more in favor of keeping security-critical parts in the vendor code (3500 contributors have more security knowledge than most dev teams using Symfony). We must make it very clear that a user should not change the cryptographically secure random generator algorithm, otherwise they open themselves to vulnerabilities (as attackers can see the algorithm, they might be able to predict the value).

Comment on lines 30 to 33
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the cookie is found, it should contain the same value as the CsrfToken

If I'm reading the implementation correctly, this is not the case. The name of the cookie must be equal to the CsrfToken prefixed with the cookie name, the value can be anything you like.

I can't directly think of a reason why the implementation was chosen to be like this, but we need to update the comment if this is indeed the logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the token in the name of the cookie prevents race conditions.
About the wording, it's approximate, but not incorrect. The cookie is both its name and its value. And either of them can contain the token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any doc or howto (not talking about the recipe) for this JavaScript snippet on the client side?
if not bounded for example with turbo or whatever high level JS lib ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recipe is the doc on the topic. We can add a link after its merged.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolas-grekas for projects that implement manual CSRF protection (e.g. with the csrf_token() twig function to manually generate a token, and some verification in the controllers), it would be great to update the recipe with an example of a Twig template showing how to construct the hidden crsf form field, so that the sample .js in the recipe can work as is (e.g. what initial values to use for the required data-attributes?) - I'm trying to get it to work on a simple login form, but I'm having trouble with the cookie value that seems to be ignored during the validation.

@nicolas-grekas
Copy link
Member Author

@wouterj this double-submit is not only a fallback. It's also a defense in-depth. What I mean is that if Origin is broken for any reasons, users won't be immediately exposed.

About the naming, "double-submit" suits me, but I also wondered about naming this "same-origin": SameOriginCsrfManager, same_origin_token_ids, etc. I could change if that's the consensus.

@nicolas-grekas
Copy link
Member Author

Any other feedback @symfony/mergers ? I'm going to merge as is without any :)

@Tobion
Copy link
Contributor

Tobion commented Oct 3, 2024

I agree that same-origin is a better name to me than double-submit because it's the main idea and is the better known term that is part of the http spec.

@javiereguiluz javiereguiluz added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Oct 3, 2024
@wouterj
Copy link
Member

wouterj commented Oct 7, 2024

I also prefer "same origin CSRF manager" over double-submit 👍

Other than that, this PR is fine with me. But we need to write detailed docs about the pros and cons of all CSRF protection mechanism during stabilization phase :)

@stof
Copy link
Member

stof commented Oct 7, 2024

One argument for renaming: it will avoid getting more reports in the future from people telling us that OWASP discourages the double submit pattern, because they miss that they only discourage naive double-submit.

@nicolas-grekas nicolas-grekas changed the title [Security] Implement double-submit CSRF protection [Security] Implement stateless headers/cookies-based CSRF protection Oct 8, 2024
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Oct 8, 2024

Renamed to SameSiteCsrfTokenManager.

The option is named stateless_token_ids:

framework:
    csrf_protection:
        stateless_token_ids: [my_stateless_token_id]

@stof
Copy link
Member

stof commented Oct 8, 2024

@nicolas-grekas is it same site or same origin ? Those have different meaning.

@wouterj
Copy link
Member

wouterj commented Oct 8, 2024

Good one, I believe it's indeed same-origin (exact match of scheme+hostname, including subdomain).

@nicolas-grekas
Copy link
Member Author

Indeed, good catch, fixed!

@@ -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'],
Copy link
Member

@GromNaN GromNaN Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The autocomplete attribute with value off on an hidden field seems invalid: #59294

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #59296

nicolas-grekas added a commit that referenced this pull request Feb 10, 2025
… default CSRF token id apply only to the app; not to bundles (nicolas-grekas)

This PR was merged into the 7.2 branch.

Discussion
----------

[Form][FrameworkBundle] Use auto-configuration to make the default CSRF token id apply only to the app; not to bundles

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

After EasyCorp/EasyAdminBundle#6724, I realized I made a mistake in #58095:

The `framework.form.csrf_protection.token_id` config option should not configure the default CSRF token id for *all* forms. Instead, we want this option to apply only to forms managed by the app. Bundles shouldn't be affected.

This is what this PR does: it switches from global config to auto-configured form types only (which means app's form types).

Commits
-------

bf1e312 [Form][FrameworkBundle] Use auto-configuration to make the default CSRF token id apply only to the app; not to bundles
kbond added a commit to symfony/maker-bundle that referenced this pull request Mar 26, 2025
…double-submit CSRF protection (nicolas-grekas)

This PR was merged into the 1.x-dev branch.

Discussion
----------

[make:security:form-login] Tweak login forms to enable double-submit CSRF protection

Will be leveraged by symfony/symfony#58095
Does no harm being enabled regardless of the Symfony version in use.

---
Waiting on:
- [x] symfony/symfony#58095

Commits
-------

b13f0fb Generate data-controller="csrf-protection" on CSRF fields
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Ready Security ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.