Skip to content
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

proposal: x/net/xsrftoken: add new, less error-prone API #42166

Open
empijei opened this issue Oct 23, 2020 · 16 comments
Open

proposal: x/net/xsrftoken: add new, less error-prone API #42166

empijei opened this issue Oct 23, 2020 · 16 comments

Comments

@empijei
Copy link
Contributor

empijei commented Oct 23, 2020

See #42166 (comment) for concrete API.


Preface

The only thing that Go provides to protect against CSRF is the x/net/xsrftoken package. This is per-se an issue but there is an additional problem.

The API of said package is the following:

const Timeout
func Generate(key, userID, actionID string) string
func Valid(token, key, userID, actionID string) bool

This is the whole API surface, it is very low-level and requires its users to have a somewhat advanced knowledge of web security to use it properly.
I would invite the reader to stop here for a handful of seconds and think how they would use this package to protect a web application, including how they would retrieve the required userID.

The issue

By looking at the naming here a programmer might be inclined to use the XSRF protection only for authenticated users, especially since userID is the name of one of the parameters for both Generate and Valid.

This means that users of this package will probably be vulnerable to Login CSRF. I say this because some colleagues of mine and I analyzed quite a lot of Go web services code we could access and found it consistently vulnerable to some form of CSRF (the maintainers have already been warned and have fixed the issues).

I propose to apply one or more of the following:

  1. Provide a higher-level protection alongside the low-level one: something that works on http handlers and manages cookies/token injection transparently. This would remove the burden of understanding the internals of CSRF from the users and would require less code to be written. As an example there are NoSurf and csrf that have a quite small but significantly harder to misuse API
  2. Add documentation to this package with examples and detailed explanation on how to use it, especially wrt pseudonymous tokens to pass as userID, which from our analysis was one of the most frequent mistakes.
  3. Provide other functions: add some functions to this package. For example helpers to issue and validate csrf-related cookies and inject tokens in html templates. These would basically be the easy-to-use and slightly more versatile building blocks for the first point of this list.

I am willing to do the work for any of these, but I would like to discuss all alternatives and gather some consensus and more ideas before I do.

@gopherbot gopherbot added this to the Unreleased milestone Oct 23, 2020
@cagedmantis cagedmantis added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Oct 26, 2020
@empijei
Copy link
Contributor Author

empijei commented Nov 23, 2020

@rsc you asked (in #42168)

what did you have in mind as a new, less error-prone API?

My proposal would be in three parts

A higher-level protection:

func Protect(http.Handler) http.Handler

This would decorate the given handler with a few features:

  • Validate CSRF tokens on state-changing methods
  • Inject the CSRF token in the request context for handlers to use

The tokens and validation algorithms we can use are several.

Double-submit secret

A secret token needs to both be in the request cookie and in a form/header. This is easy to implement and to work with. The implementation would be completely application-agnostic since it wouldn't require the user or action parameters, the CSRF protection is applied per-session.

Using this protection would require users to protect the entire ServeMux with the decorator we provide and do one of the following:

  • Add a hidden form input to all forms with the injected CSRF token value or set up the client-side code to add an additional header on requests (e.g. Angular does exactly this by default).
  • Make sure GET, HEAD, OPTIONS and similar non-state-changing methods are indeed non-state-changing.

Going for this solution would mean completely dropping the current API of this package and use a different approach altogether.

An example implementation would look like this:

package xsrftoken

// Protect defends a handler from CSRF attacks.
// This should ideally be used on entire server muxes.
// Users should make sure form-submission handlers only accept POST or other state-changing methods and don't work with GET
func Protect(h http.Handler /*+ parameters for configuration like header vs form submission*/) http.Handler{
  return http.HandlerFunc(func(w http.ResponseWriter, r*http.Request)){
    token, err := getTokenFromCookie(r)
    if err != nil {
      token = genSecureRandomToken()
      setCookie(w) // we can make this expire after 24h to keep renewing the secret
    }
    r = r.WithContext(context.WithValue(r.Context(), ctxKey, token))
    if isStateChangingMethod(r) && !valid(r){
      http.Error(w, "Forbidden", 403)
      return
    }
    h.ServeHTTP(w, r)
  })
}

// GetToken retrieves the CSRF token from the current request context.
func GetToken(r *http.Request) (string, error)

We could potentially simplify this further to rely on SameSite=strict behavior of cookies but that would introduce some niche vulnerabilities that I would avoid if possible.

User-bound token

This is more tricky to use and it's what our current API suggests to do: basically the token is re-generated when received for the given (user,action,time) tuple and validated against the received one.
The issue with this approach is that this requires quite a lot of knowledge about the app being protected for virtually no additional security.

Using this protection would require users to protect every handler in a specific way:

  • Somehow pipe a notion of "user" into the middleware
  • Somehow provide a concept of "action" to the token generation, which will have to depend on the endpoint receiving the form submission not the one generating it.
  • Add a hidden form input to all forms with the injected CSRF token value
  • Make sure GET, HEAD, OPTIONS and similar non-state-changing methods are indeed non-state-changing.

I am not particularly fond of this solution. The threat model of protecting some actions when a token is leaked for other actions seems quite odd and I don't think this extra security bit it's worth the extra cost.

Documentation

I would provide clear examples on how to properly use the currently existing functions (with code) and discourage their use in favor of the higher-level ones we are going to implement.

Note

The issue with the GET vs POST part is that we have to allow some form submissions to work (namely the non-state-changing ones like a search action) but in Go (*http.Request).FormValue retrieves the form value regardless the location (body or url).
This means we have to be very clear to our users on the need for filtering the method they accept.

There is no CSRF protection mechanism that I am aware of that would work for GET without breaking the application.

@rsc rsc changed the title x/net/xsrftoken: API is error prone proposal: x/net/xsrftoken: add new, less error-prone API Dec 2, 2020
@rsc
Copy link
Contributor

rsc commented Dec 2, 2020

Merged #42168 into this one.

@ianlancetaylor ianlancetaylor modified the milestones: Unreleased, Proposal Dec 2, 2020
@ianlancetaylor ianlancetaylor removed the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Dec 2, 2020
@ianlancetaylor ianlancetaylor added this to Incoming in Proposals (old) Dec 2, 2020
@rsc rsc moved this from Incoming to Active in Proposals (old) Dec 2, 2020
@rsc
Copy link
Contributor

rsc commented Dec 9, 2020

Some kind of automatic func Protect(h http.Handler) http.Handler sounds great.
But where is the key?
Or does this only work for single-server services?

@empijei
Copy link
Contributor Author

empijei commented Dec 16, 2020

If we use double-submit strategies (e.g. a cookie must match a header, or a cookie must match a form value) AFAIK there is no need to do key management or have server-side secrets, it is sufficient to generate random tokens for the cookie on the first visit and generate all forms tokens to match that cookie.

This is, for example, how Google's Angular protects from XSRF.

This, as you say, also has the benefit of relieving users from the burden of protecting and rotating keys.

Note: with this any server that runs on the origin the cookie was emitted for will be able to validate requests, so multiple servers behind a load balancer will be able to validate requests and generate valid forms and sessions with no communication.

If instead you need to make servers belonging to different origins generate valid forms (odd, but not impossible) you need to create a CORS endpoint with Allow-Credentials set to true and Allow-Origin set to the trusted third party that reflects the XSRF cookie in the response.
If needed we can also implement that and have an empty-by-default allow-list.

@rsc
Copy link
Contributor

rsc commented Dec 16, 2020

@empijei I don't actually understand what you are saying in the details, but the outcome seems to be that you are saying

func Protect(h http.Handler) http.Handler 

is a sufficient API to provide users something that works well even for large, multi-server services.

Assuming that's true, then does anyone object to this very simple API?

@empijei
Copy link
Contributor Author

empijei commented Dec 21, 2020

Yes, that would be a sufficient API even for multi-server services.

If users also need a multi-origin service (rare, but not impossible) it would also be trivial to add one more handler that offers that feature.

@rsc
Copy link
Contributor

rsc commented Jan 6, 2021

@empijei, over in proposal review we are still confused about the magic that allows creation of non-forgeable tokens with multiple servers and no shared secret. Maybe it would make sense to send a CL with a unit test for the load-balancer case where the client gets the form from one server and sends it back to a different one? Thanks.

@jameshartig
Copy link
Contributor

jameshartig commented Jan 6, 2021

My understanding based on the linked Angular page:

In a common anti-XSRF technique, the application server sends a randomly generated authentication token in a cookie. The client code reads the cookie and adds a custom request header with the token in all subsequent requests. The server compares the received cookie value to the request header value and rejects the request if the values are missing or don't match.

This technique is effective because all browsers implement the same origin policy. Only code from the website on which cookies are set can read the cookies from that site and set custom headers on requests to that site. That means only your application can read this cookie token and set the custom header. The malicious code on evil.com can't.

So basically it relies on the fact that the browser only allows custom headers for same-origin (unless allowed by CORS) calls. I'm not sure what happens in older browsers that don't support CORS but do support AJAX calls. It could be further secured if the user's browser implements SameSite attribute but this definitely isn't supported by those same older browsers. If I'm not mistaken, this can't rely on form values and MUST rely on a custom header because form fields could be set in a cross-origin way (such as POSTing to a new window). This would require that the user use FormData to capture the value of the form, add the header, and then send it using XMLHttpRequest.

How would you do rotation safely? There's a problem where the client gets the cookie value 123456 and stores it in a variable but at the same time an unrelated request ends up rotating the cookie to 1234567. Now when the client makes the call there could be a mismatch in cookie value versus locally-stored value. It feels risky to never expire the value in case there is some leak or vulnerability. There would need to be strict guidelines on how to implement it on the front-end.

@empijei
Copy link
Contributor Author

empijei commented Jan 7, 2021

There are a few interesting points here, thanks for your reply. Please see my comments below:

If I'm not mistaken, this can't rely on form values and MUST rely on a custom header because form fields could be set in a cross-origin way (such as POSTing to a new window)

There are two parts in this: the secrecy of the value and the protection of the transmission channel. In the Angular case the token is secret because localStorage is protected by same origin policy (SOP) and it is transmitted via a channel that is hard to circumvent, but not impossible, which is a custom header. Those older browsers that you talk about support plugins like Flash, Java applets and ActiveX, which more or less allow you to craft arbitrary headers and content types in cross-origin requests. The protection mechanism is not just relying on the transmission method but also on the fact that the token is impossible to guess.

In my proposal we send the token twice: once as a cookie (we can set this as SameSite but this is just an additional mitigation) and once in another way: either a form (this will require a mask to protect against BREACH but this is beyond this point and it's an implementation detail) or a custom header.

So basically an attacker would need to do one of two things to bypass this protection:

  1. Craft a form in which one of the fields matches a cookie on another origin (so read a cross-origin cookie)
  2. Change the cookie for another origin

Both of which are made impossible if SOP is enforced.

Here is an example of a request flight:

  1. User visits origin.org for the first time
  2. Server sets a CSRF cookie to r4nd0m (which might be set as SameSite but I don't think this is actually useful) and generates the HTML page. For every form element in the page it adds a hidden input field with the (masked) value of r4ndom.
  3. User interacts with the application and submits a form
  4. Server gets the request, unmasks the field from the form and validates it matches the cookie

Or, alternatively:
3) User interacts with the application that, via JS, adds a custom header to every request with the CSRF token value (this will require the cookie to not be HttpOnly)
4) Server gets the request and validates the header matches the cookie

(Note that we can support both by default)

How would you do rotation safely? There's a problem where the client gets the cookie value 123456 and stores it in a variable but at the same time an unrelated request ends up rotating the cookie to 1234567.

It all depends on what kind of guarantees we want to provide. What I had in mind worked like this: when we emit the cookie we also store in it when we emitted it. If we get a request that has an "old" cookie we renew it for 24 more hours (or however long is the validity window) by setting the cookie to the same value and a refreshed validity.

This means that as long as a user interacts with an application the token is "kept alive". This also means that a token might live longer than a day in case a user keeps using the application (which is something we can discuss the tradeoffs of).
If the user stops using the application for a prolonged period of time (in this example 24 hours) the generated forms will become invalid (exactly as xsrftoken package does right now, and same for most CSRF protections out there). Note that they will stop working all at once and not one after the other depending on when they were generated, which I think is a bit more consistent.

So if a user loads a page, fills a form, does not hit "submit", stops working on the tab for 24 hours and then hits "submit" there will be an error. This would also happen if we signed the tokens and set an expiration date on them, and I don't see a way to avoid this problem without having tokens live forever.

When the cookie expires the users (or some automated script on the client) will need to reload pages that contain forms.

If instead the XSRF token management is done by JS the code should read the cookie on submission instead of relying on a (potentially stale) variable.

As an alternative mechanism we could rely on signing to generate tokens, but it comes with a series of issues like key management and rotation. I'd rather have a mechanism that if compromised allows attackers to re-use a CSRF token for one user rather than all of them (if, for example, a key is compromised).

I personally consider that the risk of having a token live a long time for an always-active user is acceptable if it allows us to get rid of keys.

@andig
Copy link
Contributor

andig commented Jan 8, 2021

I cannot say that I‘m fully following the discussion, but what would speak against implementing this solution as an add-on library? That would also give room for additional requirements like multi-origin mentioned above.
If it helps clarifying the current api: could parameters like userID be renamed without breaking the compatibility promise (signature stays intact)?

@empijei
Copy link
Contributor Author

empijei commented Jan 11, 2021

what would speak against implementing this solution as an add-on library?

what do you mean by "add-on"? You mean an external repo? If so there already are several in the ecosystem, but I'd like for the stdlib to provide something instead of just making it necessary to rely on external libraries to build a secure server.

give room for additional requirements like multi-origin mentioned above

We can have that in the stdlib too

could parameters like userID be renamed without breaking the compatibility promise (signature stays intact)?

Yes, but it would hardly make this an easy-to-get-right library.

@rsc
Copy link
Contributor

rsc commented Jan 13, 2021

Thanks for the explanation @empijei. It still seems like it would help to see a CL. Is this going to be a substantial amount of code?

@rsc
Copy link
Contributor

rsc commented Jan 20, 2021

Ping @empijei. It still seems like a simple demo of the code would be most helpful.

@rsc
Copy link
Contributor

rsc commented Jan 27, 2021

I see google/go-safeweb#237, which looks like it will be the prototype/demo for this proposal. Putting this proposal on hold until that code is ready for us to look at. Please change back to the active column once it is ready. Thanks!

@rsc rsc moved this from Active to Hold in Proposals (old) Jan 27, 2021
@rsc
Copy link
Contributor

rsc commented Jan 27, 2021

Placed on hold.
— rsc for the proposal review group

@empijei
Copy link
Contributor Author

empijei commented Feb 8, 2021

Yeah, let's see how that works out with that experiment and revisit when we have some data on the efficacy and adoption of the mechanism.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Hold
Development

No branches or pull requests

8 participants