pax_global_header 0000666 0000000 0000000 00000000064 14521574447 0014527 g ustar 00root root 0000000 0000000 52 comment=a009743572494ccbc9d159005bdc58b86a44ddba csrf-1.7.2/ 0000775 0000000 0000000 00000000000 14521574447 0012473 5 ustar 00root root 0000000 0000000 csrf-1.7.2/.editorconfig 0000664 0000000 0000000 00000000504 14521574447 0015147 0 ustar 00root root 0000000 0000000 ; https://editorconfig.org/ root = true [*] insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = space indent_size = 2 [{Makefile,go.mod,go.sum,*.go,.gitmodules}] indent_style = tab indent_size = 4 [*.md] indent_size = 4 trim_trailing_whitespace = false eclint_indent_style = unset csrf-1.7.2/.github/ 0000775 0000000 0000000 00000000000 14521574447 0014033 5 ustar 00root root 0000000 0000000 csrf-1.7.2/.github/workflows/ 0000775 0000000 0000000 00000000000 14521574447 0016070 5 ustar 00root root 0000000 0000000 csrf-1.7.2/.github/workflows/issues.yml 0000664 0000000 0000000 00000000735 14521574447 0020133 0 ustar 00root root 0000000 0000000 # Add all the issues created to the project. name: Add issue or pull request to Project on: issues: types: - opened pull_request_target: types: - opened - reopened jobs: add-to-project: runs-on: ubuntu-latest steps: - name: Add issue to project uses: actions/add-to-project@v0.5.0 with: project-url: https://github.com/orgs/gorilla/projects/4 github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} csrf-1.7.2/.github/workflows/security.yml 0000664 0000000 0000000 00000001356 14521574447 0020467 0 ustar 00root root 0000000 0000000 name: Security on: push: branches: - main pull_request: branches: - main permissions: contents: read jobs: scan: strategy: matrix: go: ['1.20','1.21'] fail-fast: true runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v3 - name: Setup Go ${{ matrix.go }} uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} cache: false - name: Run GoSec uses: securego/gosec@master with: args: -exclude-dir examples ./... - name: Run GoVulnCheck uses: golang/govulncheck-action@v1 with: go-version-input: ${{ matrix.go }} go-package: ./... csrf-1.7.2/.github/workflows/test.yml 0000664 0000000 0000000 00000001370 14521574447 0017573 0 ustar 00root root 0000000 0000000 name: Test on: push: branches: - main pull_request: branches: - main permissions: contents: read jobs: unit: strategy: matrix: go: ['1.20','1.21'] os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: true runs-on: ${{ matrix.os }} steps: - name: Checkout Code uses: actions/checkout@v3 - name: Setup Go ${{ matrix.go }} uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} cache: false - name: Run Tests run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: files: ./coverage csrf-1.7.2/.github/workflows/verify.yml 0000664 0000000 0000000 00000001150 14521574447 0020114 0 ustar 00root root 0000000 0000000 name: Verify on: push: branches: - main pull_request: branches: - main permissions: contents: read jobs: lint: strategy: matrix: go: ['1.20','1.21'] fail-fast: true runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v3 - name: Setup Go ${{ matrix.go }} uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} cache: false - name: Run GolangCI-Lint uses: golangci/golangci-lint-action@v3 with: version: v1.53 args: --timeout=5m csrf-1.7.2/.gitignore 0000664 0000000 0000000 00000000026 14521574447 0014461 0 ustar 00root root 0000000 0000000 coverage.coverprofile csrf-1.7.2/Gopkg.lock 0000664 0000000 0000000 00000001326 14521574447 0014416 0 ustar 00root root 0000000 0000000 # 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 csrf-1.7.2/Gopkg.toml 0000664 0000000 0000000 00000001365 14521574447 0014444 0 ustar 00root root 0000000 0000000 # 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" csrf-1.7.2/LICENSE 0000664 0000000 0000000 00000002711 14521574447 0013501 0 ustar 00root root 0000000 0000000 Copyright (c) 2023 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: * 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. csrf-1.7.2/Makefile 0000664 0000000 0000000 00000001666 14521574447 0014144 0 ustar 00root root 0000000 0000000 GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest GO_SEC=$(shell which gosec 2> /dev/null || echo '') GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest .PHONY: golangci-lint golangci-lint: $(if $(GO_LINT), ,go install $(GO_LINT_URI)) @echo "##### Running golangci-lint" golangci-lint run -v .PHONY: gosec gosec: $(if $(GO_SEC), ,go install $(GO_SEC_URI)) @echo "##### Running gosec" gosec ./... .PHONY: govulncheck govulncheck: $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) @echo "##### Running govulncheck" govulncheck ./... .PHONY: verify verify: golangci-lint gosec govulncheck .PHONY: test test: @echo "##### Running tests" go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... csrf-1.7.2/README.md 0000664 0000000 0000000 00000032620 14521574447 0013755 0 ustar 00root root 0000000 0000000 # gorilla/csrf  [](https://codecov.io/github/gorilla/csrf) [](https://godoc.org/github.com/gorilla/csrf) [](https://sourcegraph.com/github.com/gorilla/csrf?badge)  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). ## Contents * [Install](#install) * [Examples](#examples) + [HTML Forms](#html-forms) + [JavaScript Applications](#javascript-applications) + [Google App Engine](#google-app-engine) + [Setting SameSite](#setting-samesite) + [Setting Options](#setting-options) * [Design Notes](#design-notes) * [License](#license) ## 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 - persist across application restarts. - kept secret from potential malicious users - do not hardcode it into the source code, especially not in open-source applications. 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.TrustedOrigins([]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)) } ``` ### Cookie path By default, CSRF cookies are set on the path of the request. This can create issues, if the request is done from one path to a different path. You might want to set up a root path for all the cookies; that way, the CSRF will always work across all your paths. ``` CSRF := csrf.Protect( []byte("a-32-byte-long-key-goes-here"), csrf.Path("/"), ) ``` ### 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. csrf-1.7.2/context.go 0000664 0000000 0000000 00000001053 14521574447 0014505 0 ustar 00root root 0000000 0000000 //go:build go1.7 // +build go1.7 package csrf import ( "context" "fmt" "net/http" ) func contextGet(r *http.Request, key string) (interface{}, error) { val := r.Context().Value(key) if val == nil { return nil, fmt.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) // nolint:staticcheck return r.WithContext(ctx) } func contextClear(r *http.Request) { // no-op for go1.7+ } csrf-1.7.2/csrf.go 0000664 0000000 0000000 00000022535 14521574447 0013766 0 ustar 00root root 0000000 0000000 package csrf import ( "errors" "fmt" "net/http" "net/url" "github.com/gorilla/securecookie" ) // CSRF token length in bytes. const tokenLength = 32 // Context/session keys & prefixes const ( tokenKey string = "gorilla.csrf.Token" // #nosec G101 formKey string = "gorilla.csrf.Form" // #nosec G101 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 sets the `SameSite` cookie attribute, which is // invalid in some older browsers due to changes in the SameSite spec. These // browsers will not send the cookie to the server. // csrf uses SameSiteLaxMode (SameSite=Lax) as the default as of v1.7.0+ SameSiteDefaultMode SameSiteMode = iota + 1 SameSiteLaxMode SameSiteStrictMode SameSiteNoneMode ) type csrf struct { h http.Handler sc *securecookie.SecureCookie 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 { r = envError(r, ErrBadReferer) cs.opts.ErrorHandler.ServeHTTP(w, r) return } } // Retrieve the combined token (pad + masked) token... maskedToken, err := cs.requestToken(r) if err != nil { r = envError(r, ErrBadToken) cs.opts.ErrorHandler.ServeHTTP(w, r) return } if maskedToken == nil { r = envError(r, ErrNoToken) cs.opts.ErrorHandler.ServeHTTP(w, r) return } // ... and unmask it. requestToken := unmask(maskedToken) // 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) } csrf-1.7.2/csrf_test.go 0000664 0000000 0000000 00000026172 14521574447 0015026 0 ustar 00root root 0000000 0000000 package csrf import ( "net/http" "net/http/httptest" "strings" "testing" ) var testKey = []byte("keep-it-secret-keep-it-safe-----") var testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) // TestProtect is a high-level test to make sure the middleware returns the // wrapped handler with a 200 OK status. func TestProtect(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p := Protect(testKey)(s) p.ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("middleware failed to pass to the next handler: got %v want %v", rr.Code, http.StatusOK) } if rr.Header().Get("Set-Cookie") == "" { t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie")) } cookie := rr.Header().Get("Set-Cookie") if !strings.Contains(cookie, "HttpOnly") || !strings.Contains(cookie, "Secure") { t.Fatalf("cookie does not default to Secure & HttpOnly: got %v", cookie) } } // TestCookieOptions is a test to make sure the middleware correctly sets cookie options func TestCookieOptions(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p := Protect(testKey, CookieName("nameoverride"), Secure(false), HttpOnly(false), Path("/pathoverride"), Domain("domainoverride"), MaxAge(173))(s) p.ServeHTTP(rr, r) if rr.Header().Get("Set-Cookie") == "" { t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie")) } cookie := rr.Header().Get("Set-Cookie") if strings.Contains(cookie, "HttpOnly") { t.Fatalf("cookie does not respect HttpOnly option: got %v do not want HttpOnly", cookie) } if strings.Contains(cookie, "Secure") { t.Fatalf("cookie does not respect Secure option: got %v do not want Secure", cookie) } if !strings.Contains(cookie, "nameoverride=") { t.Fatalf("cookie does not respect CookieName option: got %v want %v", cookie, "nameoverride=") } if !strings.Contains(cookie, "Domain=domainoverride") { t.Fatalf("cookie does not respect Domain option: got %v want %v", cookie, "Domain=domainoverride") } if !strings.Contains(cookie, "Max-Age=173") { t.Fatalf("cookie does not respect MaxAge option: got %v want %v", cookie, "Max-Age=173") } } // Test that idempotent methods return a 200 OK status and that non-idempotent // methods return a 403 Forbidden status when a CSRF cookie is not present. func TestMethods(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) p := Protect(testKey)(s) // Test idempontent ("safe") methods for _, method := range safeMethods { r, err := http.NewRequest(method, "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("middleware failed to pass to the next handler: got %v want %v", rr.Code, http.StatusOK) } if rr.Header().Get("Set-Cookie") == "" { t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie")) } } // Test non-idempotent methods (should return a 403 without a cookie set) nonIdempotent := []string{"POST", "PUT", "DELETE", "PATCH"} for _, method := range nonIdempotent { r, err := http.NewRequest(method, "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusForbidden { t.Fatalf("middleware failed to pass to the next handler: got %v want %v", rr.Code, http.StatusOK) } if rr.Header().Get("Set-Cookie") == "" { t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie")) } } } // Tests for failure if the cookie containing the session does not exist on a // POST request. func TestNoCookie(t *testing.T) { s := http.NewServeMux() p := Protect(testKey)(s) // POST the token back in the header. r, err := http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusForbidden { t.Fatalf("middleware failed to reject a non-existent cookie: got %v want %v", rr.Code, http.StatusForbidden) } } // TestBadCookie tests for failure when a cookie header is modified (malformed). func TestBadCookie(t *testing.T) { s := http.NewServeMux() p := Protect(testKey)(s) var token string s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { token = Token(r) })) // Obtain a CSRF cookie via a GET request. r, err := http.NewRequest("GET", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } // Replace the cookie prefix badHeader := strings.Replace(cookieName+"=", rr.Header().Get("Set-Cookie"), "_badCookie", -1) r.Header.Set("Cookie", badHeader) r.Header.Set("X-CSRF-Token", token) r.Header.Set("Referer", "http://www.gorillatoolkit.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusForbidden { t.Fatalf("middleware failed to reject a bad cookie: got %v want %v", rr.Code, http.StatusForbidden) } } // Responses should set a "Vary: Cookie" header to protect client/proxy caching. func TestVaryHeader(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) p := Protect(testKey)(s) r, err := http.NewRequest("HEAD", "https://www.golang.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("middleware failed to pass to the next handler: got %v want %v", rr.Code, http.StatusOK) } if rr.Header().Get("Vary") != "Cookie" { t.Fatalf("vary header not set: got %q want %q", rr.Header().Get("Vary"), "Cookie") } } // Requests with no Referer header should fail. func TestNoReferer(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) p := Protect(testKey)(s) r, err := http.NewRequest("POST", "https://golang.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusForbidden { t.Fatalf("middleware failed reject an empty Referer header: got %v want %v", rr.Code, http.StatusForbidden) } } // TestBadReferer checks that HTTPS requests with a Referer that does not // match the request URL correctly fail CSRF validation. func TestBadReferer(t *testing.T) { s := http.NewServeMux() p := Protect(testKey)(s) var token string s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { token = Token(r) })) // Obtain a CSRF cookie via a GET request. r, err := http.NewRequest("GET", "https://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. r, err = http.NewRequest("POST", "https://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } setCookie(rr, r) r.Header.Set("X-CSRF-Token", token) // Set a non-matching Referer header. r.Header.Set("Referer", "http://golang.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusForbidden { t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v", rr.Code, http.StatusForbidden) } } // TestTrustedReferer checks that HTTPS requests with a Referer that does not // match the request URL correctly but is a trusted origin pass CSRF validation. func TestTrustedReferer(t *testing.T) { testTable := []struct { trustedOrigin []string shouldPass bool }{ {[]string{"golang.org"}, true}, {[]string{"api.example.com", "golang.org"}, true}, {[]string{"http://golang.org"}, false}, {[]string{"https://golang.org"}, false}, {[]string{"http://example.com"}, false}, {[]string{"example.com"}, false}, } for _, item := range testTable { s := http.NewServeMux() p := Protect(testKey, TrustedOrigins(item.trustedOrigin))(s) var token string s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { token = Token(r) })) // Obtain a CSRF cookie via a GET request. r, err := http.NewRequest("GET", "https://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. r, err = http.NewRequest("POST", "https://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } setCookie(rr, r) r.Header.Set("X-CSRF-Token", token) // Set a non-matching Referer header. r.Header.Set("Referer", "http://golang.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) if item.shouldPass { if rr.Code != http.StatusOK { t.Fatalf("middleware failed to pass to the next handler: got %v want %v", rr.Code, http.StatusOK) } } else { if rr.Code != http.StatusForbidden { t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v", rr.Code, http.StatusForbidden) } } } } // Requests with a valid Referer should pass. func TestWithReferer(t *testing.T) { s := http.NewServeMux() p := Protect(testKey)(s) var token string s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { token = Token(r) })) // Obtain a CSRF cookie via a GET request. r, err := http.NewRequest("GET", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } setCookie(rr, r) r.Header.Set("X-CSRF-Token", token) r.Header.Set("Referer", "http://www.gorillatoolkit.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) if rr.Code != http.StatusOK { t.Fatalf("middleware failed to pass to the next handler: got %v want %v", rr.Code, http.StatusOK) } } // Requests without a token should fail with ErrNoToken. func TestNoTokenProvided(t *testing.T) { var finalErr error s := http.NewServeMux() p := Protect(testKey, ErrorHandler(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { finalErr = FailureReason(r) })))(s) var token string s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { token = Token(r) })) // Obtain a CSRF cookie via a GET request. r, err := http.NewRequest("GET", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) if err != nil { t.Fatal(err) } setCookie(rr, r) // By accident we use the wrong header name for the token... r.Header.Set("X-CSRF-nekot", token) r.Header.Set("Referer", "http://www.gorillatoolkit.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) if finalErr != nil && finalErr != ErrNoToken { t.Fatalf("middleware failed to return correct error: got '%v' want '%v'", finalErr, ErrNoToken) } } func setCookie(rr *httptest.ResponseRecorder, r *http.Request) { r.Header.Set("Cookie", rr.Header().Get("Set-Cookie")) } csrf-1.7.2/doc.go 0000664 0000000 0000000 00000013614 14521574447 0013574 0 ustar 00root root 0000000 0000000 /* 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 = `
See Console and Network tabs of your browser's Developer Tools for further details
Note that the X-CSRF-Token
value is in the Axios config.headers
;
it is not a response header set by the server.