From 72bb1ac7ccb66fe1b6d200517fb55d87eefcdc3c Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Mon, 4 May 2020 16:26:00 +0100 Subject: [PATCH] Set SameSite policy on CSRF cookies --- server/http.go | 5 +- vendor/github.com/gorilla/csrf/README.md | 125 +++++++++++++++++- .../github.com/gorilla/csrf/context_legacy.go | 28 ---- vendor/github.com/gorilla/csrf/csrf.go | 48 ++++++- vendor/github.com/gorilla/csrf/go.mod | 5 +- vendor/github.com/gorilla/csrf/go.sum | 6 + vendor/github.com/gorilla/csrf/options.go | 45 ++++++- vendor/github.com/gorilla/csrf/store.go | 4 + .../github.com/gorilla/csrf/store_legacy.go | 86 ++++++++++++ vendor/vendor.json | 6 +- 10 files changed, 310 insertions(+), 48 deletions(-) delete mode 100644 vendor/github.com/gorilla/csrf/context_legacy.go create mode 100644 vendor/github.com/gorilla/csrf/go.sum create mode 100644 vendor/github.com/gorilla/csrf/store_legacy.go diff --git a/server/http.go b/server/http.go index 30a0c14..6f0a033 100644 --- a/server/http.go +++ b/server/http.go @@ -156,7 +156,10 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi apph := httputil.WithDynamicHeaders(loginh, contentSecurityPolicy) if config.CSRFSecret != "" { - apph = csrf.Protect([]byte(config.CSRFSecret))(apph) + apph = csrf.Protect( + []byte(config.CSRFSecret), + csrf.SameSite(csrf.SameSiteStrictMode), + )(apph) } // Add CORS headers on the main IDP endpoints. diff --git a/vendor/github.com/gorilla/csrf/README.md b/vendor/github.com/gorilla/csrf/README.md index 86aae4a..3c7b533 100644 --- a/vendor/github.com/gorilla/csrf/README.md +++ b/vendor/github.com/gorilla/csrf/README.md @@ -1,6 +1,9 @@ # gorilla/csrf -[](https://godoc.org/github.com/gorilla/csrf) [](https://travis-ci.org/gorilla/csrf) [](https://sourcegraph.com/github.com/gorilla/csrf?badge) +[](https://godoc.org/github.com/gorilla/csrf) +[](https://sourcegraph.com/github.com/gorilla/csrf?badge) +[](https://houndci.com) +[](https://circleci.com/gh/gorilla/csrf) gorilla/csrf is a HTTP middleware library that provides [cross-site request forgery](http://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/) (CSRF) @@ -39,6 +42,7 @@ go get github.com/gorilla/csrf - [HTML Forms](#html-forms) - [JavaScript Apps](#javascript-applications) - [Google App Engine](#google-app-engine) +- [Setting SameSite](#setting-samesite) - [Setting Options](#setting-options) gorilla/csrf is easy to use: add the middleware to your router with @@ -117,7 +121,15 @@ body. ### JavaScript Applications This approach is useful if you're using a front-end JavaScript framework like -React, Ember or Angular, or are providing a JSON API. +React, Ember or Angular, and are providing a JSON API. Specifically, we need +to provide a way for our front-end fetch/AJAX calls to pass the token on each +fetch (AJAX/XMLHttpRequest) request. We achieve this by: + +- Parsing the token from the `<input>` field generated by the + `csrf.TemplateField(r)` helper, or passing it back in a response header. +- Sending this token back on every request +- Ensuring our cookie is attached to the request so that the form/header + value can be compared to the cookie value. We'll also look at applying selective CSRF protection using [gorilla/mux's](https://www.gorillatoolkit.org/pkg/mux) sub-routers, @@ -133,12 +145,13 @@ import ( func main() { r := mux.NewRouter() + csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key")) api := r.PathPrefix("/api").Subrouter() + api.Use(csrfMiddleware) api.HandleFunc("/user/{id}", GetUser).Methods("GET") - http.ListenAndServe(":8000", - csrf.Protect([]byte("32-byte-long-auth-key"))(r)) + http.ListenAndServe(":8000", r) } func GetUser(w http.ResponseWriter, r *http.Request) { @@ -159,11 +172,82 @@ func GetUser(w http.ResponseWriter, r *http.Request) { } ``` +In our JavaScript application, we should read the token from the response +headers and pass it in a request header for all requests. Here's what that +looks like when using [Axios](https://github.com/axios/axios), a popular +JavaScript HTTP client library: + +```js +// You can alternatively parse the response header for the X-CSRF-Token, and +// store that instead, if you followed the steps above to write the token to a +// response header. +let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value + +// via https://github.com/axios/axios#creating-an-instance +const instance = axios.create({ + baseURL: "https://example.com/api/", + timeout: 1000, + headers: { "X-CSRF-Token": csrfToken } +}) + +// Now, any HTTP request you make will include the csrfToken from the page, +// provided you update the csrfToken variable for each render. +try { + let resp = await instance.post(endpoint, formData) + // Do something with resp +} catch (err) { + // Handle the exception +} +``` + +If you plan to host your JavaScript application on another domain, you can use the Trusted Origins +feature to allow the host of your JavaScript application to make requests to your Go application. Observe the example below: + + +```go +package main + +import ( + "github.com/gorilla/csrf" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"), csrf.TrustedOrigin([]string{"ui.domain.com"})) + + api := r.PathPrefix("/api").Subrouter() + api.Use(csrfMiddleware) + api.HandleFunc("/user/{id}", GetUser).Methods("GET") + + http.ListenAndServe(":8000", r) +} + +func GetUser(w http.ResponseWriter, r *http.Request) { + // Authenticate the request, get the id from the route params, + // and fetch the user from the DB, etc. + + // Get the token and pass it in the CSRF header. Our JSON-speaking client + // or JavaScript framework can now read the header and return the token in + // in its own "X-CSRF-Token" request header on the subsequent POST. + w.Header().Set("X-CSRF-Token", csrf.Token(r)) + b, err := json.Marshal(user) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + w.Write(b) +} +``` + +On the example above, you're authorizing requests from `ui.domain.com` to make valid CSRF requests to your application, so you can have your API server on another domain without problems. + ### Google App Engine If you're using [Google App Engine](https://cloud.google.com/appengine/docs/go/how-requests-are-handled#Go_Requests_and_HTTP), -which doesn't allow you to hook into the default `http.ServeMux` directly, +(first-generation) which doesn't allow you to hook into the default `http.ServeMux` directly, you can still use gorilla/csrf (and gorilla/mux): ```go @@ -180,6 +264,34 @@ func init() { } ``` +Note: You can ignore this if you're using the +[second-generation](https://cloud.google.com/appengine/docs/go/) Go runtime +on App Engine (Go 1.11 and above). + +### Setting SameSite + +Go 1.11 introduced the option to set the SameSite attribute in cookies. This is +valuable if a developer wants to instruct a browser to not include cookies during +a cross site request. SameSiteStrictMode prevents all cross site requests from including +the cookie. SameSiteLaxMode prevents CSRF prone requests (POST) from including the cookie +but allows the cookie to be included in GET requests to support external linking. + +```go +func main() { + CSRF := csrf.Protect( + []byte("a-32-byte-long-key-goes-here"), + // instruct the browser to never send cookies during cross site requests + csrf.SameSite(csrf.SameSiteStrictMode), + ) + + r := mux.NewRouter() + r.HandleFunc("/signup", GetSignupForm) + r.HandleFunc("/signup/post", PostSignupForm) + + http.ListenAndServe(":8000", CSRF(r)) +} +``` + ### Setting Options What about providing your own error handler and changing the HTTP header the @@ -227,6 +339,9 @@ Getting CSRF protection right is important, so here's some background: - Cookies are authenticated and based on the [securecookie](https://github.com/gorilla/securecookie) library. They're also Secure (issued over HTTPS only) and are HttpOnly by default, because sane defaults are important. +- Cookie SameSite attribute (prevents cookies from being sent by a browser + during cross site requests) are not set by default to maintain backwards compatibility + for legacy systems. The SameSite attribute can be set with the SameSite option. - Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens and the one-time-pad used for masking them. diff --git a/vendor/github.com/gorilla/csrf/context_legacy.go b/vendor/github.com/gorilla/csrf/context_legacy.go deleted file mode 100644 index f88c9eb..0000000 --- a/vendor/github.com/gorilla/csrf/context_legacy.go +++ /dev/null @@ -1,28 +0,0 @@ -// +build !go1.7 - -package csrf - -import ( - "net/http" - - "github.com/gorilla/context" - - "github.com/pkg/errors" -) - -func contextGet(r *http.Request, key string) (interface{}, error) { - if val, ok := context.GetOk(r, key); ok { - return val, nil - } - - return nil, errors.Errorf("no value exists in the context for key %q", key) -} - -func contextSave(r *http.Request, key string, val interface{}) *http.Request { - context.Set(r, key, val) - return r -} - -func contextClear(r *http.Request) { - context.Clear(r) -} diff --git a/vendor/github.com/gorilla/csrf/csrf.go b/vendor/github.com/gorilla/csrf/csrf.go index cc7878f..f21e0a2 100644 --- a/vendor/github.com/gorilla/csrf/csrf.go +++ b/vendor/github.com/gorilla/csrf/csrf.go @@ -52,6 +52,26 @@ var ( ErrBadToken = errors.New("CSRF token invalid") ) +// SameSiteMode allows a server to define a cookie attribute making it impossible for +// the browser to send this cookie along with cross-site requests. The main +// goal is to mitigate the risk of cross-origin information leakage, and provide +// some protection against cross-site request forgery attacks. +// +// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. +type SameSiteMode int + +// SameSite options +const ( + // SameSiteDefaultMode sets the `SameSite` cookie attribute, which is + // invalid in some older browsers due to changes in the SameSite spec. These + // browsers will not send the cookie to the server. + // csrf uses SameSiteLaxMode (SameSite=Lax) as the default as of v1.7.0+ + SameSiteDefaultMode SameSiteMode = iota + 1 + SameSiteLaxMode + SameSiteStrictMode + SameSiteNoneMode +) + type csrf struct { h http.Handler sc *securecookie.SecureCookie @@ -66,12 +86,14 @@ type options struct { Path string // Note that the function and field names match the case of the associated // http.Cookie field instead of the "correct" HTTPOnly name that golint suggests. - HttpOnly bool - Secure bool - RequestHeader string - FieldName string - ErrorHandler http.Handler - CookieName string + HttpOnly bool + Secure bool + SameSite SameSiteMode + RequestHeader string + FieldName string + ErrorHandler http.Handler + CookieName string + TrustedOrigins []string } // Protect is HTTP middleware that provides Cross-Site Request Forgery @@ -165,6 +187,7 @@ func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler { maxAge: cs.opts.MaxAge, secure: cs.opts.Secure, httpOnly: cs.opts.HttpOnly, + sameSite: cs.opts.SameSite, path: cs.opts.Path, domain: cs.opts.Domain, sc: cs.sc, @@ -233,7 +256,18 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if sameOrigin(r.URL, referer) == false { + valid := sameOrigin(r.URL, referer) + + if !valid { + for _, trustedOrigin := range cs.opts.TrustedOrigins { + if referer.Host == trustedOrigin { + valid = true + break + } + } + } + + if valid == false { r = envError(r, ErrBadReferer) cs.opts.ErrorHandler.ServeHTTP(w, r) return diff --git a/vendor/github.com/gorilla/csrf/go.mod b/vendor/github.com/gorilla/csrf/go.mod index 2d2ce4d..23a5c6e 100644 --- a/vendor/github.com/gorilla/csrf/go.mod +++ b/vendor/github.com/gorilla/csrf/go.mod @@ -1,7 +1,8 @@ module github.com/gorilla/csrf require ( - github.com/gorilla/context v1.1.1 github.com/gorilla/securecookie v1.1.1 - github.com/pkg/errors v0.8.0 + github.com/pkg/errors v0.9.1 ) + +go 1.13 diff --git a/vendor/github.com/gorilla/csrf/go.sum b/vendor/github.com/gorilla/csrf/go.sum new file mode 100644 index 0000000..ff4965c --- /dev/null +++ b/vendor/github.com/gorilla/csrf/go.sum @@ -0,0 +1,6 @@ +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/vendor/github.com/gorilla/csrf/options.go b/vendor/github.com/gorilla/csrf/options.go index b50ebd4..c61d301 100644 --- a/vendor/github.com/gorilla/csrf/options.go +++ b/vendor/github.com/gorilla/csrf/options.go @@ -1,12 +1,15 @@ package csrf -import "net/http" +import ( + "net/http" +) // Option describes a functional option for configuring the CSRF handler. type Option func(*csrf) // MaxAge sets the maximum age (in seconds) of a CSRF token's underlying cookie. -// Defaults to 12 hours. +// Defaults to 12 hours. Call csrf.MaxAge(0) to explicitly set session-only +// cookies. func MaxAge(age int) Option { return func(cs *csrf) { cs.opts.MaxAge = age @@ -58,6 +61,26 @@ func HttpOnly(h bool) Option { } } +// SameSite sets the cookie SameSite attribute. Defaults to blank to maintain +// backwards compatibility, however, Strict is recommended. +// +// SameSite(SameSiteStrictMode) will prevent the cookie from being sent by the +// browser to the target site in all cross-site browsing context, even when +// following a regular link (GET request). +// +// SameSite(SameSiteLaxMode) provides a reasonable balance between security and +// usability for websites that want to maintain user's logged-in session after +// the user arrives from an external link. The session cookie would be allowed +// when following a regular link from an external website while blocking it in +// CSRF-prone request methods (e.g. POST). +// +// This option is only available for go 1.11+. +func SameSite(s SameSiteMode) Option { + return func(cs *csrf) { + cs.opts.SameSite = s + } +} + // ErrorHandler allows you to change the handler called when CSRF request // processing encounters an invalid token or request. A typical use would be to // provide a handler that returns a static HTML file with a HTTP 403 status. By @@ -97,6 +120,17 @@ func CookieName(name string) Option { } } +// TrustedOrigins configures a set of origins (Referers) that are considered as trusted. +// This will allow cross-domain CSRF use-cases - e.g. where the front-end is served +// from a different domain than the API server - to correctly pass a CSRF check. +// +// You should only provide origins you own or have full control over. +func TrustedOrigins(origins []string) Option { + return func(cs *csrf) { + cs.opts.TrustedOrigins = origins + } +} + // setStore sets the store used by the CSRF middleware. // Note: this is private (for now) to allow for internal API changes. func setStore(s store) Option { @@ -118,6 +152,13 @@ func parseOptions(h http.Handler, opts ...Option) *csrf { cs.opts.Secure = true cs.opts.HttpOnly = true + // Set SameSite=Lax by default, allowing the CSRF cookie to only be sent on + // top-level navigations. + cs.opts.SameSite = SameSiteLaxMode + + // Default; only override this if the package user explicitly calls MaxAge(0) + cs.opts.MaxAge = defaultAge + // Range over each options function and apply it // to our csrf type to configure it. Options functions are // applied in order, with any conflicting options overriding diff --git a/vendor/github.com/gorilla/csrf/store.go b/vendor/github.com/gorilla/csrf/store.go index 39f47ad..f7997fc 100644 --- a/vendor/github.com/gorilla/csrf/store.go +++ b/vendor/github.com/gorilla/csrf/store.go @@ -1,3 +1,5 @@ +// +build go1.11 + package csrf import ( @@ -28,6 +30,7 @@ type cookieStore struct { path string domain string sc *securecookie.SecureCookie + sameSite SameSiteMode } // Get retrieves a CSRF token from the session cookie. It returns an empty token @@ -63,6 +66,7 @@ func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error { MaxAge: cs.maxAge, HttpOnly: cs.httpOnly, Secure: cs.secure, + SameSite: http.SameSite(cs.sameSite), Path: cs.path, Domain: cs.domain, } diff --git a/vendor/github.com/gorilla/csrf/store_legacy.go b/vendor/github.com/gorilla/csrf/store_legacy.go new file mode 100644 index 0000000..b211164 --- /dev/null +++ b/vendor/github.com/gorilla/csrf/store_legacy.go @@ -0,0 +1,86 @@ +// +build !go1.11 +// file for compatibility with go versions prior to 1.11 + +package csrf + +import ( + "net/http" + "time" + + "github.com/gorilla/securecookie" +) + +// store represents the session storage used for CSRF tokens. +type store interface { + // Get returns the real CSRF token from the store. + Get(*http.Request) ([]byte, error) + // Save stores the real CSRF token in the store and writes a + // cookie to the http.ResponseWriter. + // For non-cookie stores, the cookie should contain a unique (256 bit) ID + // or key that references the token in the backend store. + // csrf.GenerateRandomBytes is a helper function for generating secure IDs. + Save(token []byte, w http.ResponseWriter) error +} + +// cookieStore is a signed cookie session store for CSRF tokens. +type cookieStore struct { + name string + maxAge int + secure bool + httpOnly bool + path string + domain string + sc *securecookie.SecureCookie + sameSite SameSiteMode +} + +// Get retrieves a CSRF token from the session cookie. It returns an empty token +// if decoding fails (e.g. HMAC validation fails or the named cookie doesn't exist). +func (cs *cookieStore) Get(r *http.Request) ([]byte, error) { + // Retrieve the cookie from the request + cookie, err := r.Cookie(cs.name) + if err != nil { + return nil, err + } + + token := make([]byte, tokenLength) + // Decode the HMAC authenticated cookie. + err = cs.sc.Decode(cs.name, cookie.Value, &token) + if err != nil { + return nil, err + } + + return token, nil +} + +// Save stores the CSRF token in the session cookie. +func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error { + // Generate an encoded cookie value with the CSRF token. + encoded, err := cs.sc.Encode(cs.name, token) + if err != nil { + return err + } + + cookie := &http.Cookie{ + Name: cs.name, + Value: encoded, + MaxAge: cs.maxAge, + HttpOnly: cs.httpOnly, + Secure: cs.secure, + Path: cs.path, + Domain: cs.domain, + } + + // Set the Expires field on the cookie based on the MaxAge + // If MaxAge <= 0, we don't set the Expires attribute, making the cookie + // session-only. + if cs.maxAge > 0 { + cookie.Expires = time.Now().Add( + time.Duration(cs.maxAge) * time.Second) + } + + // Write the authenticated cookie to the response. + http.SetCookie(w, cookie) + + return nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index ee40850..4aa78a0 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -111,10 +111,10 @@ "revisionTime": "2018-10-12T15:35:48Z" }, { - "checksumSHA1": "uzOh/6ll8f2HnCNHDWRenwQ/Owo=", + "checksumSHA1": "3CtHe9LMWm74S8eocCfYx0EXj/U=", "path": "github.com/gorilla/csrf", - "revision": "f903b4ea4d6056635620f6f39e930528b97f9a55", - "revisionTime": "2018-10-12T15:34:37Z" + "revision": "79c60d0e4fcf1fbc9653c1cb13d28e82248cf43c", + "revisionTime": "2020-04-26T17:13:33Z" }, { "checksumSHA1": "22kXObb09lweSbdIjPZGeLBjnkg=", -- GitLab