From 8415661bfb6812c208694b4704f75ae4499a3cd4 Mon Sep 17 00:00:00 2001 From: Miranda Christ Date: Tue, 2 Jul 2019 14:42:44 -0700 Subject: [PATCH] Add CSRF protection to deck --- go.mod | 1 + go.sum | 2 + prow/cmd/deck/BUILD.bazel | 1 + prow/cmd/deck/main.go | 34 ++- prow/cmd/deck/static/pr/pr.ts | 2 + prow/cmd/deck/static/prow/prow.ts | 6 + prow/cmd/deck/static/spyglass/spyglass.ts | 7 +- prow/cmd/deck/template/index.html | 1 + prow/cmd/deck/template/pr.html | 3 + prow/cmd/deck/template/spyglass.html | 1 + prow/cmd/deck/templates.go | 8 + repos.bzl | 6 + vendor/BUILD.bazel | 1 + vendor/github.com/gorilla/csrf/AUTHORS | 20 ++ vendor/github.com/gorilla/csrf/BUILD.bazel | 34 +++ vendor/github.com/gorilla/csrf/LICENSE | 26 ++ vendor/github.com/gorilla/csrf/context.go | 29 +++ vendor/github.com/gorilla/csrf/csrf.go | 279 +++++++++++++++++++++ vendor/github.com/gorilla/csrf/doc.go | 176 +++++++++++++ vendor/github.com/gorilla/csrf/helpers.go | 203 +++++++++++++++ vendor/github.com/gorilla/csrf/options.go | 130 ++++++++++ vendor/github.com/gorilla/csrf/store.go | 82 ++++++ 22 files changed, 1045 insertions(+), 7 deletions(-) create mode 100644 vendor/github.com/gorilla/csrf/AUTHORS create mode 100644 vendor/github.com/gorilla/csrf/BUILD.bazel create mode 100644 vendor/github.com/gorilla/csrf/LICENSE create mode 100644 vendor/github.com/gorilla/csrf/context.go create mode 100644 vendor/github.com/gorilla/csrf/csrf.go create mode 100644 vendor/github.com/gorilla/csrf/doc.go create mode 100644 vendor/github.com/gorilla/csrf/helpers.go create mode 100644 vendor/github.com/gorilla/csrf/options.go create mode 100644 vendor/github.com/gorilla/csrf/store.go diff --git a/go.mod b/go.mod index ac501e548e64b..9dccca4ab2b66 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.1.0 // indirect github.com/gophercloud/gophercloud v0.0.0-20181215224939-bdd8b1ecd793 // indirect + github.com/gorilla/csrf v1.6.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.3 github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc diff --git a/go.sum b/go.sum index 4c23850d5bd83..9a8bedb08416f 100644 --- a/go.sum +++ b/go.sum @@ -165,6 +165,8 @@ github.com/gophercloud/gophercloud v0.0.0-20181215224939-bdd8b1ecd793 h1:rT82/6k github.com/gophercloud/gophercloud v0.0.0-20181215224939-bdd8b1ecd793/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 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.0 h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= 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/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= diff --git a/prow/cmd/deck/BUILD.bazel b/prow/cmd/deck/BUILD.bazel index 3d081c273e88b..2c407aaa39676 100644 --- a/prow/cmd/deck/BUILD.bazel +++ b/prow/cmd/deck/BUILD.bazel @@ -129,6 +129,7 @@ go_library( "//prow/tide/history:go_default_library", "//vendor/cloud.google.com/go/storage:go_default_library", "//vendor/github.com/NYTimes/gziphandler:go_default_library", + "//vendor/github.com/gorilla/csrf:go_default_library", "//vendor/github.com/gorilla/sessions:go_default_library", "//vendor/github.com/prometheus/client_golang/prometheus:go_default_library", "//vendor/github.com/sirupsen/logrus:go_default_library", diff --git a/prow/cmd/deck/main.go b/prow/cmd/deck/main.go index 778086a3d7ee0..8e2c93fcb80fd 100644 --- a/prow/cmd/deck/main.go +++ b/prow/cmd/deck/main.go @@ -36,6 +36,7 @@ import ( "cloud.google.com/go/storage" "github.com/NYTimes/gziphandler" + "github.com/gorilla/csrf" "github.com/gorilla/sessions" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" @@ -93,6 +94,7 @@ type options struct { spyglassFilesLocation string gcsCredentialsFile string rerunCreatesJob bool + csrfTokenFile string } func (o *options) Validate() error { @@ -139,6 +141,8 @@ func gatherOptions(fs *flag.FlagSet, args ...string) options { fs.StringVar(&o.templateFilesLocation, "template-files-location", "/template", "Path to the template files") fs.StringVar(&o.gcsCredentialsFile, "gcs-credentials-file", "", "Path to the GCS credentials file") fs.BoolVar(&o.rerunCreatesJob, "rerun-creates-job", false, "Change the re-run option in Deck to actually create the job. **WARNING:** Only use this with non-public deck instances, otherwise strangers can DOS your Prow instance") + // csrfTokenFile, if specified, must point to a file containing a 32 byte token that will be used to protect against CSRF in POST requests + fs.StringVar(&o.csrfTokenFile, "csrf-token", "", "Path to the file containing the CSRF token.") o.kubernetes.AddFlags(fs) fs.Parse(args) o.configPath = config.ConfigPath(o.configPath) @@ -272,8 +276,6 @@ func main() { mux.Handle("/tide-history", gziphandler.GzipHandler(handleSimpleTemplate(o, cfg, "tide-history.html", nil))) mux.Handle("/plugins", gziphandler.GzipHandler(handleSimpleTemplate(o, cfg, "plugins.html", nil))) - indexHandler := handleSimpleTemplate(o, cfg, "index.html", struct{ SpyglassEnabled, ReRunCreatesJob bool }{o.spyglass, o.rerunCreatesJob}) - runLocal := o.pregeneratedData != "" var fallbackHandler func(http.ResponseWriter, *http.Request) @@ -289,6 +291,11 @@ func main() { fallbackHandler(w, r) return } + indexHandler := handleSimpleTemplate(o, cfg, "index.html", struct { + SpyglassEnabled bool + ReRunCreatesJob bool + CSRFToken string + }{o.spyglass, o.rerunCreatesJob, csrf.Token(r)}) indexHandler(w, r) }) @@ -301,8 +308,24 @@ func main() { // signal to the world that we're ready health.ServeReady() + if o.rerunCreatesJob && o.csrfTokenFile == "" { + logrus.Warning("Allowing direct reruns is susceptible to CSRF attacks. We will soon be requiring you to specify a file containing a CSRF token if --rerun-creates-job is enabled.") + } + + // if a CSRF token is specified, we protect against CSRF in all post requests + if o.csrfTokenFile != "" { + csrfToken, err := loadToken(o.csrfTokenFile) + if err != nil { + logrus.WithError(err).Fatal("Could not retrieve CSRF token.") + } + // in development, pass in csrf.Secure(false) + CSRF := csrf.Protect(csrfToken, csrf.Path("/")) + logrus.WithError(http.ListenAndServe(":8080", CSRF(traceHandler(mux)))).Fatal("ListenAndServe returned.") + return + } // setup done, actually start the server logrus.WithError(http.ListenAndServe(":8080", traceHandler(mux))).Fatal("ListenAndServe returned.") + } // localOnlyMain contains logic used only when running locally, and is mutually exclusive with @@ -724,7 +747,8 @@ func handleRequestJobViews(sg *spyglass.Spyglass, cfg config.Getter, o options) setHeadersNoCaching(w) src := strings.TrimPrefix(r.URL.Path, "/view/") - page, err := renderSpyglass(sg, cfg, src, o) + csrfToken := csrf.Token(r) + page, err := renderSpyglass(sg, cfg, src, o, csrfToken) if err != nil { logrus.WithError(err).Error("error rendering spyglass page") message := fmt.Sprintf("error rendering spyglass page: %v", err) @@ -743,7 +767,7 @@ func handleRequestJobViews(sg *spyglass.Spyglass, cfg config.Getter, o options) } // renderSpyglass returns a pre-rendered Spyglass page from the given source string -func renderSpyglass(sg *spyglass.Spyglass, cfg config.Getter, src string, o options) (string, error) { +func renderSpyglass(sg *spyglass.Spyglass, cfg config.Getter, src string, o options, csrfToken string) (string, error) { renderStart := time.Now() src = strings.TrimSuffix(src, "/") @@ -893,6 +917,7 @@ lensesLoop: TestgridLink string JobName string BuildID string + CSRFToken string ExtraLinks []spyglass.ExtraLink } lTmpl := lensesTemplate{ @@ -908,6 +933,7 @@ lensesLoop: TestgridLink: tgLink, JobName: jobName, BuildID: buildID, + CSRFToken: csrfToken, ExtraLinks: extraLinks, } t := template.New("spyglass.html") diff --git a/prow/cmd/deck/static/pr/pr.ts b/prow/cmd/deck/static/pr/pr.ts index 35c2e216b4bc3..f648aacc9d6e3 100644 --- a/prow/cmd/deck/static/pr/pr.ts +++ b/prow/cmd/deck/static/pr/pr.ts @@ -8,6 +8,7 @@ import {getCookieByName, tidehistory} from '../common/common'; declare const tideData: TideData; declare const allBuilds: Job[]; +declare const csrfToken: string; type UnifiedState = JobState | "expected" | "error" | "failure" | "pending" | "success"; @@ -115,6 +116,7 @@ function createXMLHTTPRequest(fulfillFn: (request: XMLHttpRequest) => any, error request.withCredentials = true; request.open("POST", url, true); request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + request.setRequestHeader("X-CSRF-Token", csrfToken); return request; } diff --git a/prow/cmd/deck/static/prow/prow.ts b/prow/cmd/deck/static/prow/prow.ts index 5dcef2e78641e..702c0f9ff150a 100644 --- a/prow/cmd/deck/static/prow/prow.ts +++ b/prow/cmd/deck/static/prow/prow.ts @@ -7,6 +7,7 @@ import {JobHistogram, JobSample} from './histogram'; declare const allBuilds: Job[]; declare const spyglass: boolean; declare const rerunCreatesJob: boolean; +declare const csrfToken: string; // http://stackoverflow.com/a/5158301/3694 function getParameterByName(name: string): string | null { @@ -686,6 +687,11 @@ function createRerunCell(modal: HTMLElement, rerunElement: HTMLElement, prowjob: const form = document.createElement('form'); form.method = 'POST'; form.action = `${url}`; + const tokenInput = document.createElement('input'); + tokenInput.type = 'hidden'; + tokenInput.name = 'gorilla.csrf.Token'; + tokenInput.value = csrfToken; + form.append(tokenInput); c.appendChild(form); form.submit(); }; diff --git a/prow/cmd/deck/static/spyglass/spyglass.ts b/prow/cmd/deck/static/spyglass/spyglass.ts index d28e2cc7b7163..fd1691974d57a 100644 --- a/prow/cmd/deck/static/spyglass/spyglass.ts +++ b/prow/cmd/deck/static/spyglass/spyglass.ts @@ -3,6 +3,7 @@ import { isTransitMessage } from "./common"; declare const src: string; declare const lensArtifacts: {[key: string]: string[]}; declare const lensIndexes: number[]; +declare const csrfToken: string; // Loads views for this job function loadLenses(): void { @@ -58,13 +59,13 @@ window.addEventListener('message', async (e) => { break; case "request": { const req = await fetch(urlForLensRequest(lens, index, 'callback'), - {body: message.data, method: 'POST'}); + {body: message.data, method: 'POST', headers: {'X-CSRF-Token': csrfToken}}); respond(await req.text()); break; } case "requestPage": { const req = await fetch(urlForLensRequest(lens, index, 'rerender'), - {body: message.data, method: 'POST'}); + {body: message.data, method: 'POST', headers: {'X-CSRF-Token': csrfToken}}); respond(await req.text()); break; } @@ -73,7 +74,7 @@ window.addEventListener('message', async (e) => { frame.style.visibility = 'visible'; spinner.style.display = 'block'; const req = await fetch(urlForLensRequest(lens, index, 'rerender'), - {body: message.data, method: 'POST'}); + {body: message.data, method: 'POST', headers: {'X-CSRF-Token': csrfToken}}); respond(await req.text()); break; } diff --git a/prow/cmd/deck/template/index.html b/prow/cmd/deck/template/index.html index 5543409143e7f..a2b61df900da9 100644 --- a/prow/cmd/deck/template/index.html +++ b/prow/cmd/deck/template/index.html @@ -6,6 +6,7 @@ {{end}} diff --git a/prow/cmd/deck/template/pr.html b/prow/cmd/deck/template/pr.html index 6b717538c55bb..b2424dc5c194c 100644 --- a/prow/cmd/deck/template/pr.html +++ b/prow/cmd/deck/template/pr.html @@ -5,6 +5,9 @@ + {{end}} {{define "content"}}
diff --git a/prow/cmd/deck/template/spyglass.html b/prow/cmd/deck/template/spyglass.html index 69b1a1e8f78a0..49181334fce55 100644 --- a/prow/cmd/deck/template/spyglass.html +++ b/prow/cmd/deck/template/spyglass.html @@ -5,6 +5,7 @@ var src = {{.Source}}; var lensArtifacts = {{.LensArtifacts}}; var lensIndexes = {{.LensIndexes}}; + var csrfToken = {{.CSRFToken}}; diff --git a/prow/cmd/deck/templates.go b/prow/cmd/deck/templates.go index 4ada8e1d575a8..7c04e59cf35c2 100644 --- a/prow/cmd/deck/templates.go +++ b/prow/cmd/deck/templates.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "github.com/gorilla/csrf" "github.com/sirupsen/logrus" "html/template" "k8s.io/test-infra/prow/cmd/deck/version" @@ -88,6 +89,13 @@ func handleSimpleTemplate(o options, cfg config.Getter, templateName string, par http.Error(w, "error parsing template", http.StatusInternalServerError) return } + + // ensures that the CSRF token is always passed to the template. If CSRF protection is not + // set up, the token will be an empty string. + if param == nil { + param = struct{ CSRFToken string }{csrf.Token(r)} + } + if err := t.Execute(w, param); err != nil { logrus.WithError(err).Error("error executing template " + templateName) http.Error(w, "error executing template", http.StatusInternalServerError) diff --git a/repos.bzl b/repos.bzl index a68461451d8fb..70221dc333b2a 100644 --- a/repos.bzl +++ b/repos.bzl @@ -1389,3 +1389,9 @@ def go_repositories(): sum = "h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=", version = "v1.9.1", ) + go_repository( + name = "com_github_gorilla_csrf", + importpath = "github.com/gorilla/csrf", + sum = "h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0=", + version = "v1.6.0", + ) diff --git a/vendor/BUILD.bazel b/vendor/BUILD.bazel index 14350a1562115..425aeef634bba 100644 --- a/vendor/BUILD.bazel +++ b/vendor/BUILD.bazel @@ -111,6 +111,7 @@ filegroup( "//vendor/github.com/googleapis/gnostic/extensions:all-srcs", "//vendor/github.com/gophercloud/gophercloud:all-srcs", "//vendor/github.com/gorilla/context:all-srcs", + "//vendor/github.com/gorilla/csrf:all-srcs", "//vendor/github.com/gorilla/mux:all-srcs", "//vendor/github.com/gorilla/securecookie:all-srcs", "//vendor/github.com/gorilla/sessions:all-srcs", diff --git a/vendor/github.com/gorilla/csrf/AUTHORS b/vendor/github.com/gorilla/csrf/AUTHORS new file mode 100644 index 0000000000000..4e84c37893fdc --- /dev/null +++ b/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/vendor/github.com/gorilla/csrf/BUILD.bazel b/vendor/github.com/gorilla/csrf/BUILD.bazel new file mode 100644 index 0000000000000..4ff160be159ad --- /dev/null +++ b/vendor/github.com/gorilla/csrf/BUILD.bazel @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "context.go", + "csrf.go", + "doc.go", + "helpers.go", + "options.go", + "store.go", + ], + importmap = "k8s.io/test-infra/vendor/github.com/gorilla/csrf", + importpath = "github.com/gorilla/csrf", + visibility = ["//visibility:public"], + deps = [ + "//vendor/github.com/gorilla/securecookie:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/vendor/github.com/gorilla/csrf/LICENSE b/vendor/github.com/gorilla/csrf/LICENSE new file mode 100644 index 0000000000000..c1eb344c86b0e --- /dev/null +++ b/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/vendor/github.com/gorilla/csrf/context.go b/vendor/github.com/gorilla/csrf/context.go new file mode 100644 index 0000000000000..d8bb42f00e9e8 --- /dev/null +++ b/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/vendor/github.com/gorilla/csrf/csrf.go b/vendor/github.com/gorilla/csrf/csrf.go new file mode 100644 index 0000000000000..cc7878f4fdf32 --- /dev/null +++ b/vendor/github.com/gorilla/csrf/csrf.go @@ -0,0 +1,279 @@ +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") +) + +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 + RequestHeader string + FieldName string + ErrorHandler http.Handler + CookieName 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, + 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 + } + + if sameOrigin(r.URL, referer) == 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/vendor/github.com/gorilla/csrf/doc.go b/vendor/github.com/gorilla/csrf/doc.go new file mode 100644 index 0000000000000..503c9487a5354 --- /dev/null +++ b/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/vendor/github.com/gorilla/csrf/helpers.go b/vendor/github.com/gorilla/csrf/helpers.go new file mode 100644 index 0000000000000..3dacfd21304af --- /dev/null +++ b/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/vendor/github.com/gorilla/csrf/options.go b/vendor/github.com/gorilla/csrf/options.go new file mode 100644 index 0000000000000..b50ebd4eb4f87 --- /dev/null +++ b/vendor/github.com/gorilla/csrf/options.go @@ -0,0 +1,130 @@ +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. +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 + } +} + +// 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 + } +} + +// 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 + + // 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/vendor/github.com/gorilla/csrf/store.go b/vendor/github.com/gorilla/csrf/store.go new file mode 100644 index 0000000000000..39f47ad710b6d --- /dev/null +++ b/vendor/github.com/gorilla/csrf/store.go @@ -0,0 +1,82 @@ +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 +} + +// 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 +}