diff --git a/make/photon/prepare/templates/core/app.conf.jinja b/make/photon/prepare/templates/core/app.conf.jinja index 9c92f70cd..6110364ca 100644 --- a/make/photon/prepare/templates/core/app.conf.jinja +++ b/make/photon/prepare/templates/core/app.conf.jinja @@ -4,6 +4,3 @@ enablegzip = true [dev] httpport = 8080 -EnableXSRF = true -XSRFKey = {{xsrf_key}} -XSRFExpire = 3600 diff --git a/make/photon/prepare/templates/core/env.jinja b/make/photon/prepare/templates/core/env.jinja index b863c0339..597625882 100644 --- a/make/photon/prepare/templates/core/env.jinja +++ b/make/photon/prepare/templates/core/env.jinja @@ -47,6 +47,7 @@ REGISTRY_CONTROLLER_URL={{registry_controller_url}} WITH_CHARTMUSEUM={{with_chartmuseum}} REGISTRY_CREDENTIAL_USERNAME={{registry_username}} REGISTRY_CREDENTIAL_PASSWORD={{registry_password}} +CSRF_KEY={{csrf_key}} HTTP_PROXY={{core_http_proxy}} HTTPS_PROXY={{core_https_proxy}} diff --git a/make/photon/prepare/utils/core.py b/make/photon/prepare/utils/core.py index 4d669a954..43a1f29c0 100644 --- a/make/photon/prepare/utils/core.py +++ b/make/photon/prepare/utils/core.py @@ -32,14 +32,14 @@ def prepare_core(config_dict, with_notary, with_clair, with_trivy, with_chartmus with_clair=with_clair, with_trivy=with_trivy, with_chartmuseum=with_chartmuseum, + csrf_key=generate_random_string(32), **config_dict) render_jinja( core_conf_template_path, core_conf, uid=DEFAULT_UID, - gid=DEFAULT_GID, - xsrf_key=generate_random_string(40)) + gid=DEFAULT_GID) def copy_core_config(core_templates_path, core_config_path): diff --git a/src/core/api/base.go b/src/core/api/base.go index 868a6be83..8cec5bf9c 100644 --- a/src/core/api/base.go +++ b/src/core/api/base.go @@ -83,10 +83,6 @@ func (b *BaseController) Prepare() { return } b.ProjectMgr = pm - - if !filter.ReqCarriesSession(b.Ctx.Request) { - b.EnableXSRF = false - } } // RequireAuthenticated returns true when the request is authenticated @@ -96,7 +92,6 @@ func (b *BaseController) RequireAuthenticated() bool { b.SendError(internal_errors.UnauthorizedError(errors.New("Unauthorized"))) return false } - return true } diff --git a/src/core/controllers/controllers_test.go b/src/core/controllers/controllers_test.go index 5ea60fb62..1bc397baa 100644 --- a/src/core/controllers/controllers_test.go +++ b/src/core/controllers/controllers_test.go @@ -16,6 +16,7 @@ package controllers import ( "context" "fmt" + "github.com/goharbor/harbor/src/core/middlewares" "net/http" "net/http/httptest" "os" @@ -38,7 +39,6 @@ func init() { dir := filepath.Dir(file) dir = filepath.Join(dir, "..") apppath, _ := filepath.Abs(dir) - beego.BConfig.WebConfig.EnableXSRF = true beego.BConfig.WebConfig.Session.SessionOn = true beego.TestBeegoInit(apppath) beego.AddTemplateExt("htm") @@ -100,30 +100,38 @@ func TestRedirectForOIDC(t *testing.T) { func TestAll(t *testing.T) { config.InitWithSettings(utilstest.GetUnitTestConfig()) assert := assert.New(t) + handler := http.Handler(beego.BeeApp.Handlers) + mws := middlewares.MiddleWares() + for i := len(mws) - 1; i >= 0; i-- { + if mws[i] == nil { + continue + } + handler = mws[i](handler) + } r, _ := http.NewRequest("POST", "/c/login", nil) w := httptest.NewRecorder() - beego.BeeApp.Handlers.ServeHTTP(w, r) - assert.Equal(http.StatusUnprocessableEntity, w.Code, "'/c/login' httpStatusCode should be 422") + handler.ServeHTTP(w, r) + assert.Equal(http.StatusForbidden, w.Code, "'/c/login' httpStatusCode should be 403") r, _ = http.NewRequest("GET", "/c/log_out", nil) w = httptest.NewRecorder() - beego.BeeApp.Handlers.ServeHTTP(w, r) + handler.ServeHTTP(w, r) assert.Equal(int(200), w.Code, "'/c/log_out' httpStatusCode should be 200") assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), ""), "http respond should be empty") r, _ = http.NewRequest("POST", "/c/reset", nil) w = httptest.NewRecorder() - beego.BeeApp.Handlers.ServeHTTP(w, r) - assert.Equal(http.StatusUnprocessableEntity, w.Code, "'/c/reset' httpStatusCode should be 422") + handler.ServeHTTP(w, r) + assert.Equal(http.StatusForbidden, w.Code, "'/c/reset' httpStatusCode should be 403") r, _ = http.NewRequest("POST", "/c/userExists", nil) w = httptest.NewRecorder() - beego.BeeApp.Handlers.ServeHTTP(w, r) - assert.Equal(http.StatusUnprocessableEntity, w.Code, "'/c/userExists' httpStatusCode should be 422") + handler.ServeHTTP(w, r) + assert.Equal(http.StatusForbidden, w.Code, "'/c/userExists' httpStatusCode should be 403") r, _ = http.NewRequest("GET", "/c/sendEmail", nil) w = httptest.NewRecorder() - beego.BeeApp.Handlers.ServeHTTP(w, r) + handler.ServeHTTP(w, r) assert.Equal(int(400), w.Code, "'/c/sendEmail' httpStatusCode should be 400") } diff --git a/src/core/filter/sessionchecker.go b/src/core/filter/sessionchecker.go index fe22b6ebb..99f9d0060 100644 --- a/src/core/filter/sessionchecker.go +++ b/src/core/filter/sessionchecker.go @@ -18,11 +18,8 @@ func SessionCheck(ctx *beegoctx.Context) { req := ctx.Request _, err := req.Cookie(config.SessionCookieName) if err == nil { - // This is a temp workaround for beego bug: https://github.com/goharbor/harbor/issues/10446 - // After we upgrading beego to the latest version and moving the filter to middleware, - // this workaround can be removed - *(ctx.Request) = *(req.WithContext(context.WithValue(req.Context(), SessionReqKey, true))) - log.Debug("Mark the request as no-session") + ctx.Request = req.WithContext(context.WithValue(req.Context(), SessionReqKey, true)) + log.Debugf("Mark the request as with-session: %s %s", req.Method, req.URL.RawPath) } } diff --git a/src/core/middlewares/middlewares.go b/src/core/middlewares/middlewares.go index 87ed583bd..4476adf0f 100644 --- a/src/core/middlewares/middlewares.go +++ b/src/core/middlewares/middlewares.go @@ -15,6 +15,7 @@ package middlewares import ( + "github.com/goharbor/harbor/src/server/middleware/csrf" "github.com/goharbor/harbor/src/server/middleware/readonly" "net/http" "path" @@ -71,6 +72,7 @@ func legacyAPISkipper(r *http.Request) bool { // MiddleWares returns global middlewares func MiddleWares() []beego.MiddleWare { return []beego.MiddleWare{ + csrf.Middleware(), requestid.Middleware(), readonly.Middleware(readonlySkippers...), orm.Middleware(legacyAPISkipper), diff --git a/src/core/service/notifications/base.go b/src/core/service/notifications/base.go index 8c2dad6f4..faa07b291 100644 --- a/src/core/service/notifications/base.go +++ b/src/core/service/notifications/base.go @@ -6,8 +6,3 @@ import "github.com/goharbor/harbor/src/core/api" type BaseHandler struct { api.BaseController } - -// Prepare disable the xsrf as the request is from other components and do not require the xsrf token -func (bh *BaseHandler) Prepare() { - bh.EnableXSRF = false -} diff --git a/src/core/service/token/token.go b/src/core/service/token/token.go index 571205bdf..e378363ee 100644 --- a/src/core/service/token/token.go +++ b/src/core/service/token/token.go @@ -27,13 +27,6 @@ type Handler struct { beego.Controller } -// Prepare disables xsrf for /service/token endpoint. -// This is done on purpose b/c containerd will try to send POST and fallback to GET -// more details see #10305 -func (h *Handler) Prepare() { - h.EnableXSRF = false -} - // Get handles GET request, it checks the http header for user credentials // and parse service and scope based on docker registry v2 standard, // checks the permission against local DB and generates jwt token. diff --git a/src/go.mod b/src/go.mod index 0591b7bcd..80e2a7a36 100644 --- a/src/go.mod +++ b/src/go.mod @@ -44,6 +44,7 @@ require ( github.com/google/certificate-transparency-go v1.0.21 // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect github.com/google/uuid v1.1.1 + github.com/gorilla/csrf v1.6.2 github.com/gorilla/handlers v1.3.0 github.com/gorilla/mux v1.7.2 github.com/graph-gophers/dataloader v5.0.0+incompatible diff --git a/src/go.sum b/src/go.sum index fd0112caf..3d9b69257 100644 --- a/src/go.sum +++ b/src/go.sum @@ -342,6 +342,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/csrf v1.6.2 h1:QqQ/OWwuFp4jMKgBFAzJVW3FMULdyUW7JoM4pEWuqKg= +github.com/gorilla/csrf v1.6.2/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.3.0 h1:tsg9qP3mjt1h4Roxp+M1paRjrVBfPSOpBuVclh6YluI= github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -349,6 +351,8 @@ github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +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/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= diff --git a/src/server/middleware/csrf/csrf.go b/src/server/middleware/csrf/csrf.go new file mode 100644 index 000000000..b5ebb87da --- /dev/null +++ b/src/server/middleware/csrf/csrf.go @@ -0,0 +1,89 @@ +package csrf + +import ( + "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + ierror "github.com/goharbor/harbor/src/internal/error" + serror "github.com/goharbor/harbor/src/server/error" + "github.com/goharbor/harbor/src/server/middleware" + "github.com/gorilla/csrf" + "net/http" + "os" + "strings" + "sync" +) + +const ( + csrfKeyEnv = "CSRF_KEY" + tokenHeader = "X-Harbor-CSRF-Token" + tokenCookie = "__csrf" +) + +var ( + once sync.Once + protect func(handler http.Handler) http.Handler +) + +// attachToken makes sure if csrf generate a new token it will be included in the response header +func attachToken(w http.ResponseWriter, r *http.Request) { + if t := csrf.Token(r); len(t) > 0 { + http.SetCookie(w, &http.Cookie{ + Name: tokenCookie, + Secure: true, + Value: t, + Path: "/", + SameSite: http.SameSiteStrictMode, + }) + } else { + log.Warningf("token not found in context, skip attaching") + } +} + +func handleError(w http.ResponseWriter, r *http.Request) { + attachToken(w, r) + serror.SendError(w, ierror.New(csrf.FailureReason(r)).WithCode(ierror.ForbiddenCode)) + return +} + +func attach(handler http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + attachToken(rw, req) + handler.ServeHTTP(rw, req) + }) +} + +// Middleware initialize the middleware to apply csrf selectively +func Middleware() func(handler http.Handler) http.Handler { + once.Do(func() { + key := os.Getenv(csrfKeyEnv) + if len(key) != 32 { + log.Warningf("Invalid CSRF key from environment: %s, generating random key...", key) + key = utils.GenerateRandomString() + + } + protect = csrf.Protect([]byte(key), csrf.RequestHeader(tokenHeader), + csrf.ErrorHandler(http.HandlerFunc(handleError)), + csrf.SameSite(csrf.SameSiteStrictMode), + csrf.Path("/")) + }) + return middleware.New(func(rw http.ResponseWriter, req *http.Request, next http.Handler) { + protect(attach(next)).ServeHTTP(rw, req) + }, csrfSkipper) +} + +// csrfSkipper makes sure only some of the uris accessed by non-UI client can skip the csrf check +func csrfSkipper(req *http.Request) bool { + path := req.URL.Path + // We can check the cookie directly b/c the filter and controllerRegistry is executed after middleware, so no session + // cookie is added by beego. + _, err := req.Cookie(config.SessionCookieName) + hasSession := err == nil + if (strings.HasPrefix(path, "/v2/") || + strings.HasPrefix(path, "/api/") || + strings.HasPrefix(path, "/chartrepo/") || + strings.HasPrefix(path, "/service/")) && !hasSession { + return true + } + return false +} diff --git a/src/server/middleware/csrf/csrf_test.go b/src/server/middleware/csrf/csrf_test.go new file mode 100644 index 000000000..94b000e01 --- /dev/null +++ b/src/server/middleware/csrf/csrf_test.go @@ -0,0 +1,60 @@ +package csrf + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +type handler struct { +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func TestMiddleware(t *testing.T) { + srv := Middleware()(&handler{}) + cases := []struct { + req *http.Request + statusCode int + returnToken bool + }{ + { + req: httptest.NewRequest(http.MethodGet, "/", nil), + statusCode: http.StatusOK, + returnToken: true, + }, + { + req: httptest.NewRequest(http.MethodDelete, "/", nil), + statusCode: http.StatusForbidden, + returnToken: true, + }, + { + req: httptest.NewRequest(http.MethodGet, "/api/2.0/projects", nil), // should be skipped + statusCode: http.StatusOK, + returnToken: false, + }, + { + req: httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil), // should be skipped + statusCode: http.StatusOK, + returnToken: false, + }, + } + for _, c := range cases { + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, c.req) + assert.Equal(t, c.statusCode, rec.Result().StatusCode) + assert.Equal(t, c.returnToken, hasCookie(rec.Result(), tokenCookie)) + } +} + +func hasCookie(resp *http.Response, name string) bool { + for _, c := range resp.Cookies() { + if c != nil && c.Name == name { + return true + } + } + return false +} diff --git a/src/vendor/github.com/gorilla/csrf/AUTHORS b/src/vendor/github.com/gorilla/csrf/AUTHORS new file mode 100644 index 000000000..4e84c3789 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/AUTHORS @@ -0,0 +1,20 @@ +# This is the official list of gorilla/csrf authors for copyright purposes. +# Please keep the list sorted. + +adiabatic +Google LLC (https://opensource.google.com) +jamesgroat +Joshua Carp +Kamil Kisiel +Kevin Burke +Kévin Dunglas +Kristoffer Berdal +Martin Angers +Matt Silverlock +Philip I. Thomas +Richard Musiol +Seth Hoenig +Stefano Vettorazzi +Wayne Ashley Berry +田浩浩 +陈东海 diff --git a/src/vendor/github.com/gorilla/csrf/Gopkg.lock b/src/vendor/github.com/gorilla/csrf/Gopkg.lock new file mode 100644 index 000000000..5db9540f9 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/Gopkg.lock @@ -0,0 +1,27 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/gorilla/context" + packages = ["."] + revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" + version = "v1.1" + +[[projects]] + name = "github.com/gorilla/securecookie" + packages = ["."] + revision = "667fe4e3466a040b780561fe9b51a83a3753eefc" + version = "v1.1" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "1695686bc8fa0eb76df9fe8c5ca473686071ddcf795a0595a9465a03e8ac9bef" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/src/vendor/github.com/gorilla/csrf/Gopkg.toml b/src/vendor/github.com/gorilla/csrf/Gopkg.toml new file mode 100644 index 000000000..02837b1a7 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/Gopkg.toml @@ -0,0 +1,34 @@ + +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + name = "github.com/gorilla/context" + version = "1.1.0" + +[[constraint]] + name = "github.com/gorilla/securecookie" + version = "1.1.0" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" diff --git a/src/vendor/github.com/gorilla/csrf/LICENSE b/src/vendor/github.com/gorilla/csrf/LICENSE new file mode 100644 index 000000000..c1eb344c8 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2015-2018, The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/vendor/github.com/gorilla/csrf/README.md b/src/vendor/github.com/gorilla/csrf/README.md new file mode 100644 index 000000000..3c7b5337a --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/README.md @@ -0,0 +1,352 @@ +# gorilla/csrf + +[![GoDoc](https://godoc.org/github.com/gorilla/csrf?status.svg)](https://godoc.org/github.com/gorilla/csrf) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/csrf/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/csrf?badge) +[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) +[![CircleCI](https://circleci.com/gh/gorilla/csrf.svg?style=svg)](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) +protection. It includes: + +- The `csrf.Protect` middleware/handler provides CSRF protection on routes + attached to a router or a sub-router. +- A `csrf.Token` function that provides the token to pass into your response, + whether that be a HTML form or a JSON response body. +- ... and a `csrf.TemplateField` helper that you can pass into your `html/template` + templates to replace a `{{ .csrfField }}` template tag with a hidden input + field. + +gorilla/csrf is designed to work with any Go web framework, including: + +- The [Gorilla](https://www.gorillatoolkit.org/) toolkit +- Go's built-in [net/http](http://golang.org/pkg/net/http/) package +- [Goji](https://goji.io) - see the [tailored fork](https://github.com/goji/csrf) +- [Gin](https://github.com/gin-gonic/gin) +- [Echo](https://github.com/labstack/echo) +- ... and any other router/framework that rallies around Go's `http.Handler` interface. + +gorilla/csrf is also compatible with middleware 'helper' libraries like +[Alice](https://github.com/justinas/alice) and [Negroni](https://github.com/codegangsta/negroni). + +## Install + +With a properly configured Go toolchain: + +```sh +go get github.com/gorilla/csrf +``` + +## Examples + +- [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 +the below: + +```go +CSRF := csrf.Protect([]byte("32-byte-long-auth-key")) +http.ListenAndServe(":8000", CSRF(r)) +``` + +...and then collect the token with `csrf.Token(r)` in your handlers before +passing it to the template, JSON body or HTTP header (see below). + +Note that the authentication key passed to `csrf.Protect([]byte(key))` should be +32-bytes long and persist across application restarts. Generating a random key +won't allow you to authenticate existing cookies and will break your CSRF +validation. + +gorilla/csrf inspects the HTTP headers (first) and form body (second) on +subsequent POST/PUT/PATCH/DELETE/etc. requests for the token. + +### HTML Forms + +Here's the common use-case: HTML forms you want to provide CSRF protection for, +in order to protect malicious POST requests being made: + +```go +package main + +import ( + "net/http" + + "github.com/gorilla/csrf" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + r.HandleFunc("/signup", ShowSignupForm) + // All POST requests without a valid token will return HTTP 403 Forbidden. + // We should also ensure that our mutating (non-idempotent) handler only + // matches on POST requests. We can check that here, at the router level, or + // within the handler itself via r.Method. + r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST") + + // Add the middleware to your router by wrapping it. + http.ListenAndServe(":8000", + csrf.Protect([]byte("32-byte-long-auth-key"))(r)) + // PS: Don't forget to pass csrf.Secure(false) if you're developing locally + // over plain HTTP (just don't leave it on in production). +} + +func ShowSignupForm(w http.ResponseWriter, r *http.Request) { + // signup_form.tmpl just needs a {{ .csrfField }} template tag for + // csrf.TemplateField to inject the CSRF token into. Easy! + t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{ + csrf.TemplateTag: csrf.TemplateField(r), + }) + // We could also retrieve the token directly from csrf.Token(r) and + // set it in the request header - w.Header.Set("X-CSRF-Token", token) + // This is useful if you're sending JSON to clients or a front-end JavaScript + // framework. +} + +func SubmitSignupForm(w http.ResponseWriter, r *http.Request) { + // We can trust that requests making it this far have satisfied + // our CSRF protection requirements. +} +``` + +Note that the CSRF middleware will (by necessity) consume the request body if the +token is passed via POST form values. If you need to consume this in your +handler, insert your own middleware earlier in the chain to capture the request +body. + +### JavaScript Applications + +This approach is useful if you're using a front-end JavaScript framework like +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 `` 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, +as we don't handle any POST/PUT/DELETE requests with our top-level router. + +```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")) + + 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) +} +``` + +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), +(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 +package app + +// Remember: appengine has its own package main +func init() { + r := mux.NewRouter() + r.HandleFunc("/", IndexHandler) + // ... + + // We pass our CSRF-protected router to the DefaultServeMux + http.Handle("/", csrf.Protect([]byte(your-key))(r)) +} +``` + +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 +package inspects on requests? (i.e. an existing API you're porting to Go). Well, +gorilla/csrf provides options for changing these as you see fit: + +```go +func main() { + CSRF := csrf.Protect( + []byte("a-32-byte-long-key-goes-here"), + csrf.RequestHeader("Authenticity-Token"), + csrf.FieldName("authenticity_token"), + csrf.ErrorHandler(http.HandlerFunc(serverError(403))), + ) + + r := mux.NewRouter() + r.HandleFunc("/signup", GetSignupForm) + r.HandleFunc("/signup/post", PostSignupForm) + + http.ListenAndServe(":8000", CSRF(r)) +} +``` + +Not too bad, right? + +If there's something you're confused about or a feature you would like to see +added, open an issue. + +## Design Notes + +Getting CSRF protection right is important, so here's some background: + +- This library generates unique-per-request (masked) tokens as a mitigation + against the [BREACH attack](http://breachattack.com/). +- The 'base' (unmasked) token is stored in the session, which means that + multiple browser tabs won't cause a user problems as their per-request token + is compared with the base token. +- Operates on a "whitelist only" approach where safe (non-mutating) HTTP methods + (GET, HEAD, OPTIONS, TRACE) are the _only_ methods where token validation is not + enforced. +- The design is based on the battle-tested + [Django](https://docs.djangoproject.com/en/1.8/ref/csrf/) and [Ruby on + Rails](http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html) + approaches. +- 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. + +This library does not seek to be adventurous. + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/src/vendor/github.com/gorilla/csrf/context.go b/src/vendor/github.com/gorilla/csrf/context.go new file mode 100644 index 000000000..d8bb42f00 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/context.go @@ -0,0 +1,29 @@ +// +build go1.7 + +package csrf + +import ( + "context" + "net/http" + + "github.com/pkg/errors" +) + +func contextGet(r *http.Request, key string) (interface{}, error) { + val := r.Context().Value(key) + if val == nil { + return nil, errors.Errorf("no value exists in the context for key %q", key) + } + + return val, nil +} + +func contextSave(r *http.Request, key string, val interface{}) *http.Request { + ctx := r.Context() + ctx = context.WithValue(ctx, key, val) + return r.WithContext(ctx) +} + +func contextClear(r *http.Request) { + // no-op for go1.7+ +} diff --git a/src/vendor/github.com/gorilla/csrf/csrf.go b/src/vendor/github.com/gorilla/csrf/csrf.go new file mode 100644 index 000000000..76352dee7 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/csrf.go @@ -0,0 +1,309 @@ +package csrf + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/pkg/errors" + + "github.com/gorilla/securecookie" +) + +// CSRF token length in bytes. +const tokenLength = 32 + +// Context/session keys & prefixes +const ( + tokenKey string = "gorilla.csrf.Token" + formKey string = "gorilla.csrf.Form" + errorKey string = "gorilla.csrf.Error" + skipCheckKey string = "gorilla.csrf.Skip" + cookieName string = "_gorilla_csrf" + errorPrefix string = "gorilla/csrf: " +) + +var ( + // The name value used in form fields. + fieldName = tokenKey + // defaultAge sets the default MaxAge for cookies. + defaultAge = 3600 * 12 + // The default HTTP request header to inspect + headerName = "X-CSRF-Token" + // Idempotent (safe) methods as defined by RFC7231 section 4.2.2. + safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"} +) + +// TemplateTag provides a default template tag - e.g. {{ .csrfField }} - for use +// with the TemplateField function. +var TemplateTag = "csrfField" + +var ( + // ErrNoReferer is returned when a HTTPS request provides an empty Referer + // header. + ErrNoReferer = errors.New("referer not supplied") + // ErrBadReferer is returned when the scheme & host in the URL do not match + // the supplied Referer header. + ErrBadReferer = errors.New("referer invalid") + // ErrNoToken is returned if no CSRF token is supplied in the request. + ErrNoToken = errors.New("CSRF token not found in request") + // ErrBadToken is returned if the CSRF token in the request does not match + // the token in the session, or is otherwise malformed. + 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 SameSiteMode = iota + 1 + SameSiteLaxMode + SameSiteStrictMode + SameSiteNoneMode +) + +type csrf struct { + h http.Handler + sc *securecookie.SecureCookie + st store + opts options +} + +// options contains the optional settings for the CSRF middleware. +type options struct { + MaxAge int + Domain string + 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 + SameSite SameSiteMode + RequestHeader string + FieldName string + ErrorHandler http.Handler + CookieName string + TrustedOrigins []string +} + +// Protect is HTTP middleware that provides Cross-Site Request Forgery +// protection. +// +// It securely generates a masked (unique-per-request) token that +// can be embedded in the HTTP response (e.g. form field or HTTP header). +// The original (unmasked) token is stored in the session, which is inaccessible +// by an attacker (provided you are using HTTPS). Subsequent requests are +// expected to include this token, which is compared against the session token. +// Requests that do not provide a matching token are served with a HTTP 403 +// 'Forbidden' error response. +// +// Example: +// package main +// +// import ( +// "html/template" +// +// "github.com/gorilla/csrf" +// "github.com/gorilla/mux" +// ) +// +// var t = template.Must(template.New("signup_form.tmpl").Parse(form)) +// +// func main() { +// r := mux.NewRouter() +// +// r.HandleFunc("/signup", GetSignupForm) +// // POST requests without a valid token will return a HTTP 403 Forbidden. +// r.HandleFunc("/signup/post", PostSignupForm) +// +// // Add the middleware to your router. +// http.ListenAndServe(":8000", +// // Note that the authentication key provided should be 32 bytes +// // long and persist across application restarts. +// csrf.Protect([]byte("32-byte-long-auth-key"))(r)) +// } +// +// func GetSignupForm(w http.ResponseWriter, r *http.Request) { +// // signup_form.tmpl just needs a {{ .csrfField }} template tag for +// // csrf.TemplateField to inject the CSRF token into. Easy! +// t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{ +// csrf.TemplateTag: csrf.TemplateField(r), +// }) +// // We could also retrieve the token directly from csrf.Token(r) and +// // set it in the request header - w.Header.Set("X-CSRF-Token", token) +// // This is useful if you're sending JSON to clients or a front-end JavaScript +// // framework. +// } +// +func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + cs := parseOptions(h, opts...) + + // Set the defaults if no options have been specified + if cs.opts.ErrorHandler == nil { + cs.opts.ErrorHandler = http.HandlerFunc(unauthorizedHandler) + } + + if cs.opts.MaxAge < 0 { + // Default of 12 hours + cs.opts.MaxAge = defaultAge + } + + if cs.opts.FieldName == "" { + cs.opts.FieldName = fieldName + } + + if cs.opts.CookieName == "" { + cs.opts.CookieName = cookieName + } + + if cs.opts.RequestHeader == "" { + cs.opts.RequestHeader = headerName + } + + // Create an authenticated securecookie instance. + if cs.sc == nil { + cs.sc = securecookie.New(authKey, nil) + // Use JSON serialization (faster than one-off gob encoding) + cs.sc.SetSerializer(securecookie.JSONEncoder{}) + // Set the MaxAge of the underlying securecookie. + cs.sc.MaxAge(cs.opts.MaxAge) + } + + if cs.st == nil { + // Default to the cookieStore + cs.st = &cookieStore{ + name: cs.opts.CookieName, + 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, + } + } + + return cs + } +} + +// Implements http.Handler for the csrf type. +func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Skip the check if directed to. This should always be a bool. + if val, err := contextGet(r, skipCheckKey); err == nil { + if skip, ok := val.(bool); ok { + if skip { + cs.h.ServeHTTP(w, r) + return + } + } + } + + // Retrieve the token from the session. + // An error represents either a cookie that failed HMAC validation + // or that doesn't exist. + realToken, err := cs.st.Get(r) + if err != nil || len(realToken) != tokenLength { + // If there was an error retrieving the token, the token doesn't exist + // yet, or it's the wrong length, generate a new token. + // Note that the new token will (correctly) fail validation downstream + // as it will no longer match the request token. + realToken, err = generateRandomBytes(tokenLength) + if err != nil { + r = envError(r, err) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + + // Save the new (real) token in the session store. + err = cs.st.Save(realToken, w) + if err != nil { + r = envError(r, err) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + } + + // Save the masked token to the request context + r = contextSave(r, tokenKey, mask(realToken, r)) + // Save the field name to the request context + r = contextSave(r, formKey, cs.opts.FieldName) + + // HTTP methods not defined as idempotent ("safe") under RFC7231 require + // inspection. + if !contains(safeMethods, r.Method) { + // Enforce an origin check for HTTPS connections. As per the Django CSRF + // implementation (https://goo.gl/vKA7GE) the Referer header is almost + // always present for same-domain HTTP requests. + if r.URL.Scheme == "https" { + // Fetch the Referer value. Call the error handler if it's empty or + // otherwise fails to parse. + referer, err := url.Parse(r.Referer()) + if err != nil || referer.String() == "" { + r = envError(r, ErrNoReferer) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + + 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 + } + } + + // If the token returned from the session store is nil for non-idempotent + // ("unsafe") methods, call the error handler. + if realToken == nil { + r = envError(r, ErrNoToken) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + + // Retrieve the combined token (pad + masked) token and unmask it. + requestToken := unmask(cs.requestToken(r)) + + // Compare the request token against the real token + if !compareTokens(requestToken, realToken) { + r = envError(r, ErrBadToken) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + + } + + // Set the Vary: Cookie header to protect clients from caching the response. + w.Header().Add("Vary", "Cookie") + + // Call the wrapped handler/router on success. + cs.h.ServeHTTP(w, r) + // Clear the request context after the handler has completed. + contextClear(r) +} + +// unauthorizedhandler sets a HTTP 403 Forbidden status and writes the +// CSRF failure reason to the response. +func unauthorizedHandler(w http.ResponseWriter, r *http.Request) { + http.Error(w, fmt.Sprintf("%s - %s", + http.StatusText(http.StatusForbidden), FailureReason(r)), + http.StatusForbidden) + return +} diff --git a/src/vendor/github.com/gorilla/csrf/doc.go b/src/vendor/github.com/gorilla/csrf/doc.go new file mode 100644 index 000000000..503c9487a --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/doc.go @@ -0,0 +1,176 @@ +/* +Package csrf (gorilla/csrf) provides Cross Site Request Forgery (CSRF) +prevention middleware for Go web applications & services. + +It includes: + +* The `csrf.Protect` middleware/handler provides CSRF protection on routes +attached to a router or a sub-router. + +* A `csrf.Token` function that provides the token to pass into your response, +whether that be a HTML form or a JSON response body. + +* ... and a `csrf.TemplateField` helper that you can pass into your `html/template` +templates to replace a `{{ .csrfField }}` template tag with a hidden input +field. + +gorilla/csrf is easy to use: add the middleware to individual handlers with +the below: + + CSRF := csrf.Protect([]byte("32-byte-long-auth-key")) + http.HandlerFunc("/route", CSRF(YourHandler)) + +... and then collect the token with `csrf.Token(r)` before passing it to the +template, JSON body or HTTP header (you pick!). gorilla/csrf inspects the form body +(first) and HTTP headers (second) on subsequent POST/PUT/PATCH/DELETE/etc. requests +for the token. + +Note that the authentication key passed to `csrf.Protect([]byte(key))` should be +32-bytes long and persist across application restarts. Generating a random key +won't allow you to authenticate existing cookies and will break your CSRF +validation. + +Here's the common use-case: HTML forms you want to provide CSRF protection for, +in order to protect malicious POST requests being made: + + package main + + import ( + "fmt" + "html/template" + "net/http" + + "github.com/gorilla/csrf" + "github.com/gorilla/mux" + ) + + var form = ` + + + Sign Up! + + +
+ + + + {{ .csrfField }} + +
+ + + ` + + var t = template.Must(template.New("signup_form.tmpl").Parse(form)) + + func main() { + r := mux.NewRouter() + r.HandleFunc("/signup", ShowSignupForm) + // All POST requests without a valid token will return HTTP 403 Forbidden. + // We should also ensure that our mutating (non-idempotent) handler only + // matches on POST requests. We can check that here, at the router level, or + // within the handler itself via r.Method. + r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST") + + // Add the middleware to your router by wrapping it. + http.ListenAndServe(":8000", + csrf.Protect([]byte("32-byte-long-auth-key"))(r)) + // PS: Don't forget to pass csrf.Secure(false) if you're developing locally + // over plain HTTP (just don't leave it on in production). + } + + func ShowSignupForm(w http.ResponseWriter, r *http.Request) { + // signup_form.tmpl just needs a {{ .csrfField }} template tag for + // csrf.TemplateField to inject the CSRF token into. Easy! + t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{ + csrf.TemplateTag: csrf.TemplateField(r), + }) + } + + func SubmitSignupForm(w http.ResponseWriter, r *http.Request) { + // We can trust that requests making it this far have satisfied + // our CSRF protection requirements. + fmt.Fprintf(w, "%v\n", r.PostForm) + } + +Note that the CSRF middleware will (by necessity) consume the request body if the +token is passed via POST form values. If you need to consume this in your +handler, insert your own middleware earlier in the chain to capture the request +body. + +You can also send the CSRF token in the response header. This approach is useful +if you're using a front-end JavaScript framework like Ember or Angular, or are +providing a JSON API: + + package main + + import ( + "github.com/gorilla/csrf" + "github.com/gorilla/mux" + ) + + func main() { + r := mux.NewRouter() + + api := r.PathPrefix("/api").Subrouter() + api.HandleFunc("/user/:id", GetUser).Methods("GET") + + http.ListenAndServe(":8000", + csrf.Protect([]byte("32-byte-long-auth-key"))(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) + } + +If you're writing a client that's supposed to mimic browser behavior, make sure to +send back the CSRF cookie (the default name is _gorilla_csrf, but this can be changed +with the CookieName Option) along with either the X-CSRF-Token header or the gorilla.csrf.Token form field. + +In addition: getting CSRF protection right is important, so here's some background: + +* This library generates unique-per-request (masked) tokens as a mitigation +against the BREACH attack (http://breachattack.com/). + +* The 'base' (unmasked) token is stored in the session, which means that +multiple browser tabs won't cause a user problems as their per-request token +is compared with the base token. + +* Operates on a "whitelist only" approach where safe (non-mutating) HTTP methods +(GET, HEAD, OPTIONS, TRACE) are the *only* methods where token validation is not +enforced. + +* The design is based on the battle-tested Django +(https://docs.djangoproject.com/en/1.8/ref/csrf/) and Ruby on Rails +(http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html) +approaches. + +* 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. + +* Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens +and the one-time-pad used for masking them. + +This library does not seek to be adventurous. + +*/ +package csrf diff --git a/src/vendor/github.com/gorilla/csrf/go.mod b/src/vendor/github.com/gorilla/csrf/go.mod new file mode 100644 index 000000000..a5bf9b2f7 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/go.mod @@ -0,0 +1,6 @@ +module github.com/gorilla/csrf + +require ( + github.com/gorilla/securecookie v1.1.1 + github.com/pkg/errors v0.8.0 +) diff --git a/src/vendor/github.com/gorilla/csrf/go.sum b/src/vendor/github.com/gorilla/csrf/go.sum new file mode 100644 index 000000000..2d11d997e --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/go.sum @@ -0,0 +1,4 @@ +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= diff --git a/src/vendor/github.com/gorilla/csrf/helpers.go b/src/vendor/github.com/gorilla/csrf/helpers.go new file mode 100644 index 000000000..3dacfd213 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/helpers.go @@ -0,0 +1,203 @@ +package csrf + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "html/template" + "net/http" + "net/url" +) + +// Token returns a masked CSRF token ready for passing into HTML template or +// a JSON response body. An empty token will be returned if the middleware +// has not been applied (which will fail subsequent validation). +func Token(r *http.Request) string { + if val, err := contextGet(r, tokenKey); err == nil { + if maskedToken, ok := val.(string); ok { + return maskedToken + } + } + + return "" +} + +// FailureReason makes CSRF validation errors available in the request context. +// This is useful when you want to log the cause of the error or report it to +// client. +func FailureReason(r *http.Request) error { + if val, err := contextGet(r, errorKey); err == nil { + if err, ok := val.(error); ok { + return err + } + } + + return nil +} + +// UnsafeSkipCheck will skip the CSRF check for any requests. This must be +// called before the CSRF middleware. +// +// Note: You should not set this without otherwise securing the request from +// CSRF attacks. The primary use-case for this function is to turn off CSRF +// checks for non-browser clients using authorization tokens against your API. +func UnsafeSkipCheck(r *http.Request) *http.Request { + return contextSave(r, skipCheckKey, true) +} + +// TemplateField is a template helper for html/template that provides an field +// populated with a CSRF token. +// +// Example: +// +// // The following tag in our form.tmpl template: +// {{ .csrfField }} +// +// // ... becomes: +// +// +func TemplateField(r *http.Request) template.HTML { + if name, err := contextGet(r, formKey); err == nil { + fragment := fmt.Sprintf(``, + name, Token(r)) + + return template.HTML(fragment) + } + + return template.HTML("") +} + +// mask returns a unique-per-request token to mitigate the BREACH attack +// as per http://breachattack.com/#mitigations +// +// The token is generated by XOR'ing a one-time-pad and the base (session) CSRF +// token and returning them together as a 64-byte slice. This effectively +// randomises the token on a per-request basis without breaking multiple browser +// tabs/windows. +func mask(realToken []byte, r *http.Request) string { + otp, err := generateRandomBytes(tokenLength) + if err != nil { + return "" + } + + // XOR the OTP with the real token to generate a masked token. Append the + // OTP to the front of the masked token to allow unmasking in the subsequent + // request. + return base64.StdEncoding.EncodeToString(append(otp, xorToken(otp, realToken)...)) +} + +// unmask splits the issued token (one-time-pad + masked token) and returns the +// unmasked request token for comparison. +func unmask(issued []byte) []byte { + // Issued tokens are always masked and combined with the pad. + if len(issued) != tokenLength*2 { + return nil + } + + // We now know the length of the byte slice. + otp := issued[tokenLength:] + masked := issued[:tokenLength] + + // Unmask the token by XOR'ing it against the OTP used to mask it. + return xorToken(otp, masked) +} + +// requestToken returns the issued token (pad + masked token) from the HTTP POST +// body or HTTP header. It will return nil if the token fails to decode. +func (cs *csrf) requestToken(r *http.Request) []byte { + // 1. Check the HTTP header first. + issued := r.Header.Get(cs.opts.RequestHeader) + + // 2. Fall back to the POST (form) value. + if issued == "" { + issued = r.PostFormValue(cs.opts.FieldName) + } + + // 3. Finally, fall back to the multipart form (if set). + if issued == "" && r.MultipartForm != nil { + vals := r.MultipartForm.Value[cs.opts.FieldName] + + if len(vals) > 0 { + issued = vals[0] + } + } + + // Decode the "issued" (pad + masked) token sent in the request. Return a + // nil byte slice on a decoding error (this will fail upstream). + decoded, err := base64.StdEncoding.DecodeString(issued) + if err != nil { + return nil + } + + return decoded +} + +// generateRandomBytes returns securely generated random bytes. +// It will return an error if the system's secure random number generator +// fails to function correctly. +func generateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // err == nil only if len(b) == n + if err != nil { + return nil, err + } + + return b, nil + +} + +// sameOrigin returns true if URLs a and b share the same origin. The same +// origin is defined as host (which includes the port) and scheme. +func sameOrigin(a, b *url.URL) bool { + return (a.Scheme == b.Scheme && a.Host == b.Host) +} + +// compare securely (constant-time) compares the unmasked token from the request +// against the real token from the session. +func compareTokens(a, b []byte) bool { + // This is required as subtle.ConstantTimeCompare does not check for equal + // lengths in Go versions prior to 1.3. + if len(a) != len(b) { + return false + } + + return subtle.ConstantTimeCompare(a, b) == 1 +} + +// xorToken XORs tokens ([]byte) to provide unique-per-request CSRF tokens. It +// will return a masked token if the base token is XOR'ed with a one-time-pad. +// An unmasked token will be returned if a masked token is XOR'ed with the +// one-time-pad used to mask it. +func xorToken(a, b []byte) []byte { + n := len(a) + if len(b) < n { + n = len(b) + } + + res := make([]byte, n) + + for i := 0; i < n; i++ { + res[i] = a[i] ^ b[i] + } + + return res +} + +// contains is a helper function to check if a string exists in a slice - e.g. +// whether a HTTP method exists in a list of safe methods. +func contains(vals []string, s string) bool { + for _, v := range vals { + if v == s { + return true + } + } + + return false +} + +// envError stores a CSRF error in the request context. +func envError(r *http.Request, err error) *http.Request { + return contextSave(r, errorKey, err) +} diff --git a/src/vendor/github.com/gorilla/csrf/options.go b/src/vendor/github.com/gorilla/csrf/options.go new file mode 100644 index 000000000..da8216718 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/options.go @@ -0,0 +1,170 @@ +package csrf + +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. Call csrf.MaxAge(0) to explicitly set session-only +// cookies. +func MaxAge(age int) Option { + return func(cs *csrf) { + cs.opts.MaxAge = age + } +} + +// Domain sets the cookie domain. Defaults to the current domain of the request +// only (recommended). +// +// This should be a hostname and not a URL. If set, the domain is treated as +// being prefixed with a '.' - e.g. "example.com" becomes ".example.com" and +// matches "www.example.com" and "secure.example.com". +func Domain(domain string) Option { + return func(cs *csrf) { + cs.opts.Domain = domain + } +} + +// Path sets the cookie path. Defaults to the path the cookie was issued from +// (recommended). +// +// This instructs clients to only respond with cookie for that path and its +// subpaths - i.e. a cookie issued from "/register" would be included in requests +// to "/register/step2" and "/register/submit". +func Path(p string) Option { + return func(cs *csrf) { + cs.opts.Path = p + } +} + +// Secure sets the 'Secure' flag on the cookie. Defaults to true (recommended). +// Set this to 'false' in your development environment otherwise the cookie won't +// be sent over an insecure channel. Setting this via the presence of a 'DEV' +// environmental variable is a good way of making sure this won't make it to a +// production environment. +func Secure(s bool) Option { + return func(cs *csrf) { + cs.opts.Secure = s + } +} + +// HttpOnly sets the 'HttpOnly' flag on the cookie. Defaults to true (recommended). +func HttpOnly(h bool) Option { + return func(cs *csrf) { + // Note that the function and field names match the case of the + // related http.Cookie field instead of the "correct" HTTPOnly name + // that golint suggests. + cs.opts.HttpOnly = h + } +} + +// 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 +// default a HTTP 403 status and a plain text CSRF failure reason are served. +// +// Note that a custom error handler can also access the csrf.FailureReason(r) +// function to retrieve the CSRF validation reason from the request context. +func ErrorHandler(h http.Handler) Option { + return func(cs *csrf) { + cs.opts.ErrorHandler = h + } +} + +// RequestHeader allows you to change the request header the CSRF middleware +// inspects. The default is X-CSRF-Token. +func RequestHeader(header string) Option { + return func(cs *csrf) { + cs.opts.RequestHeader = header + } +} + +// FieldName allows you to change the name attribute of the hidden field +// inspected by this package. The default is 'gorilla.csrf.Token'. +func FieldName(name string) Option { + return func(cs *csrf) { + cs.opts.FieldName = name + } +} + +// CookieName changes the name of the CSRF cookie issued to clients. +// +// Note that cookie names should not contain whitespace, commas, semicolons, +// backslashes or control characters as per RFC6265. +func CookieName(name string) Option { + return func(cs *csrf) { + cs.opts.CookieName = name + } +} + +// 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 { + return func(cs *csrf) { + cs.st = s + } +} + +// parseOptions parses the supplied options functions and returns a configured +// csrf handler. +func parseOptions(h http.Handler, opts ...Option) *csrf { + // Set the handler to call after processing. + cs := &csrf{ + h: h, + } + + // Default to true. See Secure & HttpOnly function comments for rationale. + // Set here to allow package users to override the default. + cs.opts.Secure = true + cs.opts.HttpOnly = true + + // Default to blank to maintain backwards compatibility + cs.opts.SameSite = SameSiteDefaultMode + + // 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 + // earlier calls. + for _, option := range opts { + option(cs) + } + + return cs +} diff --git a/src/vendor/github.com/gorilla/csrf/store.go b/src/vendor/github.com/gorilla/csrf/store.go new file mode 100644 index 000000000..f7997fc44 --- /dev/null +++ b/src/vendor/github.com/gorilla/csrf/store.go @@ -0,0 +1,86 @@ +// +build go1.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, + SameSite: http.SameSite(cs.sameSite), + 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/src/vendor/github.com/gorilla/csrf/store_legacy.go b/src/vendor/github.com/gorilla/csrf/store_legacy.go new file mode 100644 index 000000000..b21116431 --- /dev/null +++ b/src/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/src/vendor/github.com/gorilla/securecookie/.travis.yml b/src/vendor/github.com/gorilla/securecookie/.travis.yml new file mode 100644 index 000000000..6f440f1e4 --- /dev/null +++ b/src/vendor/github.com/gorilla/securecookie/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.3 + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: 1.7 + - go: tip + allow_failures: + - go: tip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet $(go list ./... | grep -v /vendor/) + - go test -v -race ./... diff --git a/src/vendor/github.com/gorilla/securecookie/LICENSE b/src/vendor/github.com/gorilla/securecookie/LICENSE new file mode 100644 index 000000000..0e5fb8728 --- /dev/null +++ b/src/vendor/github.com/gorilla/securecookie/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/vendor/github.com/gorilla/securecookie/README.md b/src/vendor/github.com/gorilla/securecookie/README.md new file mode 100644 index 000000000..aa7bd1a5b --- /dev/null +++ b/src/vendor/github.com/gorilla/securecookie/README.md @@ -0,0 +1,80 @@ +securecookie +============ +[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge) + + +securecookie encodes and decodes authenticated and optionally encrypted +cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. It is still +recommended that sensitive data not be stored in cookies, and that HTTPS be used +to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). + +## Examples + +To use it, first create a new SecureCookie instance: + +```go +// Hash keys should be at least 32 bytes long +var hashKey = []byte("very-secret") +// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. +// Shorter keys may weaken the encryption used. +var blockKey = []byte("a-lot-secret") +var s = securecookie.New(hashKey, blockKey) +``` + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + +```go +func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + Secure: true, + HttpOnly: true, + } + http.SetCookie(w, cookie) + } +} +``` + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + +```go +func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } +} +``` + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using `encoding/gob`. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. An optional JSON encoder that uses `encoding/json` is +available for types compatible with JSON. + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/src/vendor/github.com/gorilla/securecookie/doc.go b/src/vendor/github.com/gorilla/securecookie/doc.go new file mode 100644 index 000000000..ae89408d9 --- /dev/null +++ b/src/vendor/github.com/gorilla/securecookie/doc.go @@ -0,0 +1,61 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package securecookie encodes and decodes authenticated and optionally +encrypted cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. + +To use it, first create a new SecureCookie instance: + + var hashKey = []byte("very-secret") + var blockKey = []byte("a-lot-secret") + var s = securecookie.New(hashKey, blockKey) + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + + func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + } + } + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + + func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } + } + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using encoding/gob. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. +*/ +package securecookie diff --git a/src/vendor/github.com/gorilla/securecookie/fuzz.go b/src/vendor/github.com/gorilla/securecookie/fuzz.go new file mode 100644 index 000000000..e4d0534e4 --- /dev/null +++ b/src/vendor/github.com/gorilla/securecookie/fuzz.go @@ -0,0 +1,25 @@ +// +build gofuzz + +package securecookie + +var hashKey = []byte("very-secret12345") +var blockKey = []byte("a-lot-secret1234") +var s = New(hashKey, blockKey) + +type Cookie struct { + B bool + I int + S string +} + +func Fuzz(data []byte) int { + datas := string(data) + var c Cookie + if err := s.Decode("fuzz", datas, &c); err != nil { + return 0 + } + if _, err := s.Encode("fuzz", c); err != nil { + panic(err) + } + return 1 +} diff --git a/src/vendor/github.com/gorilla/securecookie/securecookie.go b/src/vendor/github.com/gorilla/securecookie/securecookie.go new file mode 100644 index 000000000..cd4e0976d --- /dev/null +++ b/src/vendor/github.com/gorilla/securecookie/securecookie.go @@ -0,0 +1,646 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package securecookie + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/gob" + "encoding/json" + "fmt" + "hash" + "io" + "strconv" + "strings" + "time" +) + +// Error is the interface of all errors returned by functions in this library. +type Error interface { + error + + // IsUsage returns true for errors indicating the client code probably + // uses this library incorrectly. For example, the client may have + // failed to provide a valid hash key, or may have failed to configure + // the Serializer adequately for encoding value. + IsUsage() bool + + // IsDecode returns true for errors indicating that a cookie could not + // be decoded and validated. Since cookies are usually untrusted + // user-provided input, errors of this type should be expected. + // Usually, the proper action is simply to reject the request. + IsDecode() bool + + // IsInternal returns true for unexpected errors occurring in the + // securecookie implementation. + IsInternal() bool + + // Cause, if it returns a non-nil value, indicates that this error was + // propagated from some underlying library. If this method returns nil, + // this error was raised directly by this library. + // + // Cause is provided principally for debugging/logging purposes; it is + // rare that application logic should perform meaningfully different + // logic based on Cause. See, for example, the caveats described on + // (MultiError).Cause(). + Cause() error +} + +// errorType is a bitmask giving the error type(s) of an cookieError value. +type errorType int + +const ( + usageError = errorType(1 << iota) + decodeError + internalError +) + +type cookieError struct { + typ errorType + msg string + cause error +} + +func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } +func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } +func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } + +func (e cookieError) Cause() error { return e.cause } + +func (e cookieError) Error() string { + parts := []string{"securecookie: "} + if e.msg == "" { + parts = append(parts, "error") + } else { + parts = append(parts, e.msg) + } + if c := e.Cause(); c != nil { + parts = append(parts, " - caused by: ", c.Error()) + } + return strings.Join(parts, "") +} + +var ( + errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} + + errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} + errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} + errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} + errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} + + errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} + errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} + errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} + errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} + errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} + errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} + errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} + + // ErrMacInvalid indicates that cookie decoding failed because the HMAC + // could not be extracted and verified. Direct use of this error + // variable is deprecated; it is public only for legacy compatibility, + // and may be privatized in the future, as it is rarely useful to + // distinguish between this error and other Error implementations. + ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} +) + +// Codec defines an interface to encode and decode cookie values. +type Codec interface { + Encode(name string, value interface{}) (string, error) + Decode(name, value string, dst interface{}) error +} + +// New returns a new SecureCookie. +// +// hashKey is required, used to authenticate values using HMAC. Create it using +// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes. +// +// blockKey is optional, used to encrypt values. Create it using +// GenerateRandomKey(). The key length must correspond to the block size +// of the encryption algorithm. For AES, used by default, valid lengths are +// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. +// The default encoder used for cookie serialization is encoding/gob. +// +// Note that keys created using GenerateRandomKey() are not automatically +// persisted. New keys will be created when the application is restarted, and +// previously issued cookies will not be able to be decoded. +func New(hashKey, blockKey []byte) *SecureCookie { + s := &SecureCookie{ + hashKey: hashKey, + blockKey: blockKey, + hashFunc: sha256.New, + maxAge: 86400 * 30, + maxLength: 4096, + sz: GobEncoder{}, + } + if hashKey == nil { + s.err = errHashKeyNotSet + } + if blockKey != nil { + s.BlockFunc(aes.NewCipher) + } + return s +} + +// SecureCookie encodes and decodes authenticated and optionally encrypted +// cookie values. +type SecureCookie struct { + hashKey []byte + hashFunc func() hash.Hash + blockKey []byte + block cipher.Block + maxLength int + maxAge int64 + minAge int64 + err error + sz Serializer + // For testing purposes, the function that returns the current timestamp. + // If not set, it will use time.Now().UTC().Unix(). + timeFunc func() int64 +} + +// Serializer provides an interface for providing custom serializers for cookie +// values. +type Serializer interface { + Serialize(src interface{}) ([]byte, error) + Deserialize(src []byte, dst interface{}) error +} + +// GobEncoder encodes cookie values using encoding/gob. This is the simplest +// encoder and can handle complex types via gob.Register. +type GobEncoder struct{} + +// JSONEncoder encodes cookie values using encoding/json. Users who wish to +// encode complex types need to satisfy the json.Marshaller and +// json.Unmarshaller interfaces. +type JSONEncoder struct{} + +// NopEncoder does not encode cookie values, and instead simply accepts a []byte +// (as an interface{}) and returns a []byte. This is particularly useful when +// you encoding an object upstream and do not wish to re-encode it. +type NopEncoder struct{} + +// MaxLength restricts the maximum length, in bytes, for the cookie value. +// +// Default is 4096, which is the maximum value accepted by Internet Explorer. +func (s *SecureCookie) MaxLength(value int) *SecureCookie { + s.maxLength = value + return s +} + +// MaxAge restricts the maximum age, in seconds, for the cookie value. +// +// Default is 86400 * 30. Set it to 0 for no restriction. +func (s *SecureCookie) MaxAge(value int) *SecureCookie { + s.maxAge = int64(value) + return s +} + +// MinAge restricts the minimum age, in seconds, for the cookie value. +// +// Default is 0 (no restriction). +func (s *SecureCookie) MinAge(value int) *SecureCookie { + s.minAge = int64(value) + return s +} + +// HashFunc sets the hash function used to create HMAC. +// +// Default is crypto/sha256.New. +func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { + s.hashFunc = f + return s +} + +// BlockFunc sets the encryption function used to create a cipher.Block. +// +// Default is crypto/aes.New. +func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { + if s.blockKey == nil { + s.err = errBlockKeyNotSet + } else if block, err := f(s.blockKey); err == nil { + s.block = block + } else { + s.err = cookieError{cause: err, typ: usageError} + } + return s +} + +// Encoding sets the encoding/serialization method for cookies. +// +// Default is encoding/gob. To encode special structures using encoding/gob, +// they must be registered first using gob.Register(). +func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { + s.sz = sz + + return s +} + +// Encode encodes a cookie value. +// +// It serializes, optionally encrypts, signs with a message authentication code, +// and finally encodes the value. +// +// The name argument is the cookie name. It is stored with the encoded value. +// The value argument is the value to be encoded. It can be any value that can +// be encoded using the currently selected serializer; see SetSerializer(). +// +// It is the client's responsibility to ensure that value, when encoded using +// the current serialization/encryption settings on s and then base64-encoded, +// is shorter than the maximum permissible length. +func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { + if s.err != nil { + return "", s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return "", s.err + } + var err error + var b []byte + // 1. Serialize. + if b, err = s.sz.Serialize(value); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + // 2. Encrypt (optional). + if s.block != nil { + if b, err = encrypt(s.block, b); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + } + b = encode(b) + // 3. Create MAC for "name|date|value". Extra pipe to be used later. + b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) + mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) + // Append mac, remove name. + b = append(b, mac...)[len(name)+1:] + // 4. Encode to base64. + b = encode(b) + // 5. Check length. + if s.maxLength != 0 && len(b) > s.maxLength { + return "", errEncodedValueTooLong + } + // Done. + return string(b), nil +} + +// Decode decodes a cookie value. +// +// It decodes, verifies a message authentication code, optionally decrypts and +// finally deserializes the value. +// +// The name argument is the cookie name. It must be the same name used when +// it was stored. The value argument is the encoded cookie value. The dst +// argument is where the cookie will be decoded. It must be a pointer. +func (s *SecureCookie) Decode(name, value string, dst interface{}) error { + if s.err != nil { + return s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return s.err + } + // 1. Check length. + if s.maxLength != 0 && len(value) > s.maxLength { + return errValueToDecodeTooLong + } + // 2. Decode from base64. + b, err := decode([]byte(value)) + if err != nil { + return err + } + // 3. Verify MAC. Value is "date|value|mac". + parts := bytes.SplitN(b, []byte("|"), 3) + if len(parts) != 3 { + return ErrMacInvalid + } + h := hmac.New(s.hashFunc, s.hashKey) + b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) + if err = verifyMac(h, b, parts[2]); err != nil { + return err + } + // 4. Verify date ranges. + var t1 int64 + if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { + return errTimestampInvalid + } + t2 := s.timestamp() + if s.minAge != 0 && t1 > t2-s.minAge { + return errTimestampTooNew + } + if s.maxAge != 0 && t1 < t2-s.maxAge { + return errTimestampExpired + } + // 5. Decrypt (optional). + b, err = decode(parts[1]) + if err != nil { + return err + } + if s.block != nil { + if b, err = decrypt(s.block, b); err != nil { + return err + } + } + // 6. Deserialize. + if err = s.sz.Deserialize(b, dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + // Done. + return nil +} + +// timestamp returns the current timestamp, in seconds. +// +// For testing purposes, the function that generates the timestamp can be +// overridden. If not set, it will return time.Now().UTC().Unix(). +func (s *SecureCookie) timestamp() int64 { + if s.timeFunc == nil { + return time.Now().UTC().Unix() + } + return s.timeFunc() +} + +// Authentication ------------------------------------------------------------- + +// createMac creates a message authentication code (MAC). +func createMac(h hash.Hash, value []byte) []byte { + h.Write(value) + return h.Sum(nil) +} + +// verifyMac verifies that a message authentication code (MAC) is valid. +func verifyMac(h hash.Hash, value []byte, mac []byte) error { + mac2 := createMac(h, value) + // Check that both MACs are of equal length, as subtle.ConstantTimeCompare + // does not do this prior to Go 1.4. + if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { + return nil + } + return ErrMacInvalid +} + +// Encryption ----------------------------------------------------------------- + +// encrypt encrypts a value using the given block in counter mode. +// +// A random initialization vector (http://goo.gl/zF67k) with the length of the +// block size is prepended to the resulting ciphertext. +func encrypt(block cipher.Block, value []byte) ([]byte, error) { + iv := GenerateRandomKey(block.BlockSize()) + if iv == nil { + return nil, errGeneratingIV + } + // Encrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + // Return iv + ciphertext. + return append(iv, value...), nil +} + +// decrypt decrypts a value using the given block in counter mode. +// +// The value to be decrypted must be prepended by a initialization vector +// (http://goo.gl/zF67k) with the length of the block size. +func decrypt(block cipher.Block, value []byte) ([]byte, error) { + size := block.BlockSize() + if len(value) > size { + // Extract iv. + iv := value[:size] + // Extract ciphertext. + value = value[size:] + // Decrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + return value, nil + } + return nil, errDecryptionFailed +} + +// Serialization -------------------------------------------------------------- + +// Serialize encodes a value using gob. +func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize encodes a value using encoding/json. +func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using encoding/json. +func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { + dec := json.NewDecoder(bytes.NewReader(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize passes a []byte through as-is. +func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { + if b, ok := src.([]byte); ok { + return b, nil + } + + return nil, errValueNotByte +} + +// Deserialize passes a []byte through as-is. +func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { + if dat, ok := dst.(*[]byte); ok { + *dat = src + return nil + } + return errValueNotBytePtr +} + +// Encoding ------------------------------------------------------------------- + +// encode encodes a value using base64. +func encode(value []byte) []byte { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) + base64.URLEncoding.Encode(encoded, value) + return encoded +} + +// decode decodes a cookie using base64. +func decode(value []byte) ([]byte, error) { + decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) + b, err := base64.URLEncoding.Decode(decoded, value) + if err != nil { + return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} + } + return decoded[:b], nil +} + +// Helpers -------------------------------------------------------------------- + +// GenerateRandomKey creates a random key with the given length in bytes. +// On failure, returns nil. +// +// Callers should explicitly check for the possibility of a nil return, treat +// it as a failure of the system random number generator, and not continue. +func GenerateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +// CodecsFromPairs returns a slice of SecureCookie instances. +// +// It is a convenience function to create a list of codecs for key rotation. Note +// that the generated Codecs will have the default options applied: callers +// should iterate over each Codec and type-assert the underlying *SecureCookie to +// change these. +// +// Example: +// +// codecs := securecookie.CodecsFromPairs( +// []byte("new-hash-key"), +// []byte("new-block-key"), +// []byte("old-hash-key"), +// []byte("old-block-key"), +// ) +// +// // Modify each instance. +// for _, s := range codecs { +// if cookie, ok := s.(*securecookie.SecureCookie); ok { +// cookie.MaxAge(86400 * 7) +// cookie.SetSerializer(securecookie.JSONEncoder{}) +// cookie.HashFunc(sha512.New512_256) +// } +// } +// +func CodecsFromPairs(keyPairs ...[]byte) []Codec { + codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) + for i := 0; i < len(keyPairs); i += 2 { + var blockKey []byte + if i+1 < len(keyPairs) { + blockKey = keyPairs[i+1] + } + codecs[i/2] = New(keyPairs[i], blockKey) + } + return codecs +} + +// EncodeMulti encodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { + if len(codecs) == 0 { + return "", errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + encoded, err := codec.Encode(name, value) + if err == nil { + return encoded, nil + } + errors = append(errors, err) + } + return "", errors +} + +// DecodeMulti decodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { + if len(codecs) == 0 { + return errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + err := codec.Decode(name, value, dst) + if err == nil { + return nil + } + errors = append(errors, err) + } + return errors +} + +// MultiError groups multiple errors. +type MultiError []error + +func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } +func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } +func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } + +// Cause returns nil for MultiError; there is no unique underlying cause in the +// general case. +// +// Note: we could conceivably return a non-nil Cause only when there is exactly +// one child error with a Cause. However, it would be brittle for client code +// to rely on the arity of causes inside a MultiError, so we have opted not to +// provide this functionality. Clients which really wish to access the Causes +// of the underlying errors are free to iterate through the errors themselves. +func (m MultiError) Cause() error { return nil } + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} + +// any returns true if any element of m is an Error for which pred returns true. +func (m MultiError) any(pred func(Error) bool) bool { + for _, e := range m { + if ourErr, ok := e.(Error); ok && pred(ourErr) { + return true + } + } + return false +} diff --git a/src/vendor/modules.txt b/src/vendor/modules.txt index 44f659ece..b324cacbf 100644 --- a/src/vendor/modules.txt +++ b/src/vendor/modules.txt @@ -189,10 +189,14 @@ github.com/google/go-querystring/query github.com/google/gofuzz # github.com/google/uuid v1.1.1 github.com/google/uuid +# github.com/gorilla/csrf v1.6.2 +github.com/gorilla/csrf # github.com/gorilla/handlers v1.3.0 github.com/gorilla/handlers # github.com/gorilla/mux v1.7.2 github.com/gorilla/mux +# github.com/gorilla/securecookie v1.1.1 +github.com/gorilla/securecookie # github.com/graph-gophers/dataloader v5.0.0+incompatible github.com/graph-gophers/dataloader # github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af