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
Comments
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:
The tokens and validation algorithms we can use are several. Double-submit secretA 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 Using this protection would require users to protect the entire
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 tokenThis 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 Using this protection would require users to protect every handler in a specific way:
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. DocumentationI 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. NoteThe 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 There is no CSRF protection mechanism that I am aware of that would work for GET without breaking the application. |
Merged #42168 into this one. |
Some kind of automatic func Protect(h http.Handler) http.Handler sounds great. |
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. |
@empijei I don't actually understand what you are saying in the details, but the outcome seems to be that you are saying
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? |
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. |
@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. |
My understanding based on the linked Angular page:
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 |
There are a few interesting points here, thanks for your reply. Please see my comments below:
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 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:
Both of which are made impossible if SOP is enforced. Here is an example of a request flight:
Or, alternatively: (Note that we can support both by default)
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). 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. |
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. |
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.
We can have that in the stdlib too
Yes, but it would hardly make this an easy-to-get-right library. |
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? |
Ping @empijei. It still seems like a simple demo of the code would be most helpful. |
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! |
Placed on hold. |
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. |
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:
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 bothGenerate
andValid
.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:
userID
, which from our analysis was one of the most frequent mistakes.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.
The text was updated successfully, but these errors were encountered: