diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c6b74c3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+; 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
\ No newline at end of file
diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
new file mode 100644
index 0000000..768b05b
--- /dev/null
+++ b/.github/workflows/issues.yml
@@ -0,0 +1,21 @@
+# 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 }}
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
new file mode 100644
index 0000000..ff4a613
--- /dev/null
+++ b/.github/workflows/security.yml
@@ -0,0 +1,37 @@
+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: ./...
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..50a3946
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,35 @@
+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
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000..a3eb74b
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,32 @@
+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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..84039fe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+coverage.coverprofile
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 13b50cd..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-language: go
-sudo: false
-
-matrix:
- include:
- - go: 1.2
- - go: 1.3
- - go: 1.4
- - go: 1.5
- - go: 1.6
- - go: tip
- allow_failures:
- - go: tip
-
-install:
- - # skip
-
-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/Gopkg.lock b/Gopkg.lock
new file mode 100644
index 0000000..5db9540
--- /dev/null
+++ b/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/Gopkg.toml b/Gopkg.toml
new file mode 100644
index 0000000..02837b1
--- /dev/null
+++ b/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/LICENSE b/LICENSE
index f407b7f..bb9d80b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,27 +1,27 @@
-Copyright (c) 2015, Matt Silverlock (matt@eatsleeprepeat.net) All rights
-reserved.
+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:
+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.
+ * 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.
-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.
+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/Makefile b/Makefile
new file mode 100644
index 0000000..ac37ffd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+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 ./...
diff --git a/README.md b/README.md
index 02e4f42..d768c81 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,64 @@
# gorilla/csrf
-[](https://godoc.org/github.com/gorilla/csrf) [](https://travis-ci.org/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:
+protection. It includes:
-* The `csrf.Protect` middleware/handler provides CSRF protection on routes
+- 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,
+- 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`
+- ... 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](http://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.
+- 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 Options](#setting-options)
+- [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:
@@ -50,9 +71,12 @@ 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
+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
@@ -77,7 +101,10 @@ func main() {
r := mux.NewRouter()
r.HandleFunc("/signup", ShowSignupForm)
// All POST requests without a valid token will return HTTP 403 Forbidden.
- r.HandleFunc("/signup/post", SubmitSignupForm)
+ // 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",
@@ -89,12 +116,12 @@ func main() {
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{
+ 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 your sending JSON to clients or a front-end JavaScript
+ // This is useful if you're sending JSON to clients or a front-end JavaScript
// framework.
}
@@ -112,10 +139,18 @@ body.
### JavaScript Applications
This approach is useful if you're using a front-end JavaScript framework like
-React, Ember or Angular, or are providing a JSON API.
+React, Ember or Angular, and are providing a JSON API. Specifically, we need
+to provide a way for our front-end fetch/AJAX calls to pass the token on each
+fetch (AJAX/XMLHttpRequest) request. We achieve this by:
+
+- Parsing the token from the `` 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](http://www.gorillatoolkit.org/pkg/mux) sub-routers,
+[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
@@ -128,12 +163,82 @@ import (
func main() {
r := mux.NewRouter()
+ csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))
api := r.PathPrefix("/api").Subrouter()
- api.HandleFunc("/user/:id", GetUser).Methods("GET")
+ api.Use(csrfMiddleware)
+ api.HandleFunc("/user/{id}", GetUser).Methods("GET")
- http.ListenAndServe(":8000",
- csrf.Protect([]byte("32-byte-long-auth-key"))(r))
+ http.ListenAndServe(":8000", r)
+}
+
+func GetUser(w http.ResponseWriter, r *http.Request) {
+ // 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) {
@@ -154,11 +259,13 @@ func GetUser(w http.ResponseWriter, r *http.Request) {
}
```
+On the example above, you're authorizing requests from `ui.domain.com` to make valid CSRF requests to your application, so you can have your API server on another domain without problems.
+
### Google App Engine
If you're using [Google App
Engine](https://cloud.google.com/appengine/docs/go/how-requests-are-handled#Go_Requests_and_HTTP),
-which doesn't allow you to hook into the default `http.ServeMux` directly,
+(first-generation) which doesn't allow you to hook into the default `http.ServeMux` directly,
you can still use gorilla/csrf (and gorilla/mux):
```go
@@ -175,6 +282,49 @@ func init() {
}
```
+Note: You can ignore this if you're using the
+[second-generation](https://cloud.google.com/appengine/docs/go/) Go runtime
+on App Engine (Go 1.11 and above).
+
+### Setting SameSite
+
+Go 1.11 introduced the option to set the SameSite attribute in cookies. This is
+valuable if a developer wants to instruct a browser to not include cookies during
+a cross site request. SameSiteStrictMode prevents all cross site requests from including
+the cookie. SameSiteLaxMode prevents CSRF prone requests (POST) from including the cookie
+but allows the cookie to be included in GET requests to support external linking.
+
+```go
+func main() {
+ CSRF := csrf.Protect(
+ []byte("a-32-byte-long-key-goes-here"),
+ // instruct the browser to never send cookies during cross site requests
+ csrf.SameSite(csrf.SameSiteStrictMode),
+ )
+
+ r := mux.NewRouter()
+ r.HandleFunc("/signup", GetSignupForm)
+ r.HandleFunc("/signup/post", PostSignupForm)
+
+ http.ListenAndServe(":8000", CSRF(r))
+}
+```
+
+### 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
@@ -207,22 +357,25 @@ added, open an issue.
Getting CSRF protection right is important, so here's some background:
-* This library generates unique-per-request (masked) tokens as a mitigation
+- 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
+- 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
+- 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
+- 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)
+- 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
+- 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.
diff --git a/context.go b/context.go
index d8bb42f..d24b146 100644
--- a/context.go
+++ b/context.go
@@ -1,26 +1,25 @@
+//go:build go1.7
// +build go1.7
package csrf
import (
"context"
+ "fmt"
"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 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)
+ ctx = context.WithValue(ctx, key, val) // nolint:staticcheck
return r.WithContext(ctx)
}
diff --git a/context_legacy.go b/context_legacy.go
deleted file mode 100644
index f88c9eb..0000000
--- a/context_legacy.go
+++ /dev/null
@@ -1,28 +0,0 @@
-// +build !go1.7
-
-package csrf
-
-import (
- "net/http"
-
- "github.com/gorilla/context"
-
- "github.com/pkg/errors"
-)
-
-func contextGet(r *http.Request, key string) (interface{}, error) {
- if val, ok := context.GetOk(r, key); ok {
- return val, nil
- }
-
- return nil, errors.Errorf("no value exists in the context for key %q", key)
-}
-
-func contextSave(r *http.Request, key string, val interface{}) *http.Request {
- context.Set(r, key, val)
- return r
-}
-
-func contextClear(r *http.Request) {
- context.Clear(r)
-}
diff --git a/csrf.go b/csrf.go
index b4b0439..5dda254 100644
--- a/csrf.go
+++ b/csrf.go
@@ -1,11 +1,12 @@
package csrf
import (
+ "context"
+ "errors"
"fmt"
"net/http"
"net/url"
-
- "github.com/pkg/errors"
+ "slices"
"github.com/gorilla/securecookie"
)
@@ -15,14 +16,22 @@ const tokenLength = 32
// Context/session keys & prefixes
const (
- tokenKey string = "gorilla.csrf.Token"
- formKey string = "gorilla.csrf.Form"
+ 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: "
)
+type contextKey string
+
+// PlaintextHTTPContextKey is the context key used to store whether the request
+// is being served via plaintext HTTP. This is used to signal to the middleware
+// that strict Referer checking should not be enforced as is done for HTTPS by
+// default.
+const PlaintextHTTPContextKey contextKey = "plaintext"
+
var (
// The name value used in form fields.
fieldName = tokenKey
@@ -42,6 +51,9 @@ var (
// ErrNoReferer is returned when a HTTPS request provides an empty Referer
// header.
ErrNoReferer = errors.New("referer not supplied")
+ // ErrBadOrigin is returned when the Origin header is present and is not a
+ // trusted origin.
+ ErrBadOrigin = errors.New("origin invalid")
// ErrBadReferer is returned when the scheme & host in the URL do not match
// the supplied Referer header.
ErrBadReferer = errors.New("referer invalid")
@@ -52,6 +64,26 @@ var (
ErrBadToken = errors.New("CSRF token invalid")
)
+// SameSiteMode allows a server to define a cookie attribute making it impossible for
+// the browser to send this cookie along with cross-site requests. The main
+// goal is to mitigate the risk of cross-origin information leakage, and provide
+// some protection against cross-site request forgery attacks.
+//
+// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
+type SameSiteMode int
+
+// SameSite options
+const (
+ // SameSiteDefaultMode sets the `SameSite` cookie attribute, which is
+ // invalid in some older browsers due to changes in the SameSite spec. These
+ // browsers will not send the cookie to the server.
+ // csrf uses SameSiteLaxMode (SameSite=Lax) as the default as of v1.7.0+
+ SameSiteDefaultMode SameSiteMode = iota + 1
+ SameSiteLaxMode
+ SameSiteStrictMode
+ SameSiteNoneMode
+)
+
type csrf struct {
h http.Handler
sc *securecookie.SecureCookie
@@ -66,12 +98,14 @@ type options struct {
Path string
// Note that the function and field names match the case of the associated
// http.Cookie field instead of the "correct" HTTPOnly name that golint suggests.
- HttpOnly bool
- Secure bool
- RequestHeader string
- FieldName string
- ErrorHandler http.Handler
- CookieName string
+ HttpOnly bool
+ Secure bool
+ SameSite SameSiteMode
+ RequestHeader string
+ FieldName string
+ ErrorHandler http.Handler
+ CookieName string
+ TrustedOrigins []string
}
// Protect is HTTP middleware that provides Cross-Site Request Forgery
@@ -86,39 +120,43 @@ type options struct {
// 'Forbidden' error response.
//
// Example:
+//
// package main
//
// import (
-// "github.com/elithrar/protect"
+// "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 := mux.NewRouter()
//
-// mux.HandlerFunc("/signup", GetSignupForm)
-// // POST requests without a valid token will return a HTTP 403 Forbidden.
-// mux.HandlerFunc("/signup/post", PostSignupForm)
+// 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.
+// // 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{
+// 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 your sending JSON to clients or a front-end JavaScript
+// // 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...)
@@ -161,6 +199,7 @@ func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler {
maxAge: cs.opts.MaxAge,
secure: cs.opts.Secure,
httpOnly: cs.opts.HttpOnly,
+ sameSite: cs.opts.SameSite,
path: cs.opts.Path,
domain: cs.opts.Domain,
sc: cs.sc,
@@ -216,10 +255,50 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 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" {
+ var isPlaintext bool
+ val := r.Context().Value(PlaintextHTTPContextKey)
+ if val != nil {
+ isPlaintext, _ = val.(bool)
+ }
+
+ // take a copy of the request URL to avoid mutating the original
+ // attached to the request.
+ // set the scheme & host based on the request context as these are not
+ // populated by default for server requests
+ // ref: https://pkg.go.dev/net/http#Request
+ requestURL := *r.URL // shallow clone
+
+ requestURL.Scheme = "https"
+ if isPlaintext {
+ requestURL.Scheme = "http"
+ }
+ if requestURL.Host == "" {
+ requestURL.Host = r.Host
+ }
+
+ // if we have an Origin header, check it against our allowlist
+ origin := r.Header.Get("Origin")
+ if origin != "" {
+ parsedOrigin, err := url.Parse(origin)
+ if err != nil {
+ r = envError(r, ErrBadOrigin)
+ cs.opts.ErrorHandler.ServeHTTP(w, r)
+ return
+ }
+ if !sameOrigin(&requestURL, parsedOrigin) && !slices.Contains(cs.opts.TrustedOrigins, parsedOrigin.Host) {
+ r = envError(r, ErrBadOrigin)
+ cs.opts.ErrorHandler.ServeHTTP(w, r)
+ return
+ }
+ }
+
+ // If we are serving via TLS and have no Origin header, prevent against
+ // CSRF via HTTP machine in the middle attacks by enforcing strict
+ // Referer origin checks. Consider an attacker who performs a
+ // successful HTTP Machine-in-the-Middle attack and uses this to inject
+ // a form and cause submission to our origin. We strictly disallow
+ // cleartext HTTP origins and evaluate the domain against an allowlist.
+ if origin == "" && !isPlaintext {
// Fetch the Referer value. Call the error handler if it's empty or
// otherwise fails to parse.
referer, err := url.Parse(r.Referer())
@@ -229,23 +308,39 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
- if sameOrigin(r.URL, referer) == false {
+ // disallow cleartext HTTP referers when serving via TLS
+ if referer.Scheme == "http" {
+ r = envError(r, ErrBadReferer)
+ cs.opts.ErrorHandler.ServeHTTP(w, r)
+ return
+ }
+
+ // If the request is being served via TLS and the Referer is not the
+ // same origin, check the domain against our allowlist. We only
+ // check when we have host information from the referer.
+ if referer.Host != "" && referer.Host != r.Host && !slices.Contains(cs.opts.TrustedOrigins, referer.Host) {
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 {
+ // 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
}
- // Retrieve the combined token (pad + masked) token and unmask it.
- requestToken := unmask(cs.requestToken(r))
+ // ... and unmask it.
+ requestToken := unmask(maskedToken)
// Compare the request token against the real token
if !compareTokens(requestToken, realToken) {
@@ -265,11 +360,19 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
contextClear(r)
}
+// PlaintextHTTPRequest accepts as input a http.Request and returns a new
+// http.Request with the PlaintextHTTPContextKey set to true. This is used to
+// signal to the CSRF middleware that the request is being served over plaintext
+// HTTP and that Referer-based origin allow-listing checks should be skipped.
+func PlaintextHTTPRequest(r *http.Request) *http.Request {
+ ctx := context.WithValue(r.Context(), PlaintextHTTPContextKey, true)
+ return r.WithContext(ctx)
+}
+
// 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/csrf_test.go b/csrf_test.go
index eed669c..0281680 100644
--- a/csrf_test.go
+++ b/csrf_test.go
@@ -1,6 +1,7 @@
package csrf
import (
+ "fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -16,10 +17,7 @@ func TestProtect(t *testing.T) {
s := http.NewServeMux()
s.HandleFunc("/", testHandler)
- r, err := http.NewRequest("GET", "/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("GET", "/", false)
rr := httptest.NewRecorder()
p := Protect(testKey)(s)
@@ -46,10 +44,7 @@ func TestCookieOptions(t *testing.T) {
s := http.NewServeMux()
s.HandleFunc("/", testHandler)
- r, err := http.NewRequest("GET", "/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("GET", "/", false)
rr := httptest.NewRecorder()
p := Protect(testKey, CookieName("nameoverride"), Secure(false), HttpOnly(false), Path("/pathoverride"), Domain("domainoverride"), MaxAge(173))(s)
@@ -86,10 +81,7 @@ func TestMethods(t *testing.T) {
// Test idempontent ("safe") methods
for _, method := range safeMethods {
- r, err := http.NewRequest(method, "/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest(method, "/", false)
rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)
@@ -107,10 +99,7 @@ func TestMethods(t *testing.T) {
// 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)
- }
+ r := createRequest(method, "/", false)
rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)
@@ -132,16 +121,8 @@ func TestNoCookie(t *testing.T) {
s := http.NewServeMux()
p := Protect(testKey)(s)
- var token string
- s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- token = Token(r)
- }))
-
// POST the token back in the header.
- r, err := http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("POST", "/", false)
rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)
@@ -158,24 +139,18 @@ func TestBadCookie(t *testing.T) {
p := Protect(testKey)(s)
var token string
- s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ 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)
- }
+ r := createRequest("GET", "/", false)
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)
- }
+ r = createRequest("POST", "/", false)
// Replace the cookie prefix
badHeader := strings.Replace(cookieName+"=", rr.Header().Get("Set-Cookie"), "_badCookie", -1)
@@ -198,10 +173,7 @@ func TestVaryHeader(t *testing.T) {
s.HandleFunc("/", testHandler)
p := Protect(testKey)(s)
- r, err := http.NewRequest("HEAD", "https://www.golang.org/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("GET", "/", true)
rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)
@@ -216,16 +188,13 @@ func TestVaryHeader(t *testing.T) {
}
}
-// Requests with no Referer header should fail.
+// TestNoReferer checks that HTTPS requests with no Referer header 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)
- }
+ r := createRequest("POST", "https://golang.org/", true)
rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)
@@ -243,25 +212,17 @@ func TestBadReferer(t *testing.T) {
p := Protect(testKey)(s)
var token string
- s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ 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)
- }
-
+ r := createRequest("GET", "/", true)
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)
- }
-
+ r = createRequest("POST", "/", true)
setCookie(rr, r)
r.Header.Set("X-CSRF-Token", token)
@@ -277,34 +238,88 @@ func TestBadReferer(t *testing.T) {
}
}
+// 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 {
+ t.Run(fmt.Sprintf("TrustedOrigin: %v", item.trustedOrigin), func(t *testing.T) {
+
+ 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 := createRequest("GET", "/", true)
+
+ rr := httptest.NewRecorder()
+ p.ServeHTTP(rr, r)
+
+ // POST the token back in the header.
+ r = createRequest("POST", "/", true)
+
+ setCookie(rr, r)
+ r.Header.Set("X-CSRF-Token", token)
+
+ // Set a non-matching Referer header.
+ r.Header.Set("Referer", "https://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(w http.ResponseWriter, r *http.Request) {
+ 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)
- }
-
+ r := createRequest("GET", "/", true)
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)
- }
+ r = createRequest("POST", "/", true)
setCookie(rr, r)
r.Header.Set("X-CSRF-Token", token)
- r.Header.Set("Referer", "http://www.gorillatoolkit.org/")
+ r.Header.Set("Referer", "https://www.gorillatoolkit.org/")
rr = httptest.NewRecorder()
p.ServeHTTP(rr, r)
@@ -315,6 +330,215 @@ func TestWithReferer(t *testing.T) {
}
}
+// 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 := createRequest("GET", "/", true)
+
+ rr := httptest.NewRecorder()
+ p.ServeHTTP(rr, r)
+
+ // POST the token back in the header.
+ r = createRequest("POST", "/", true)
+
+ 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", "https://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"))
}
+
+func TestProtectScenarios(t *testing.T) {
+ tests := []struct {
+ name string
+ safeMethod bool
+ originUntrusted bool
+ originHTTP bool
+ originTrusted bool
+ secureRequest bool
+ refererTrusted bool
+ refererUntrusted bool
+ refererHTTPDowngrade bool
+ refererRelative bool
+ tokenValid bool
+ tokenInvalid bool
+ want bool
+ }{
+ {
+ name: "safe method pass",
+ safeMethod: true,
+ want: true,
+ },
+ {
+ name: "cleartext POST with trusted origin & valid token pass",
+ originHTTP: true,
+ tokenValid: true,
+ want: true,
+ },
+ {
+ name: "cleartext POST with untrusted origin reject",
+ originUntrusted: true,
+ tokenValid: true,
+ },
+ {
+ name: "cleartext POST with HTTP origin & invalid token reject",
+ originHTTP: true,
+ },
+ {
+ name: "cleartext POST without origin with valid token pass",
+ tokenValid: true,
+ want: true,
+ },
+ {
+ name: "cleartext POST without origin with invalid token reject",
+ },
+ {
+ name: "TLS POST with HTTP origin & no referer & valid token reject",
+ tokenValid: true,
+ secureRequest: true,
+ originHTTP: true,
+ },
+ {
+ name: "TLS POST without origin and without referer reject",
+ secureRequest: true,
+ tokenValid: true,
+ },
+ {
+ name: "TLS POST without origin with untrusted referer reject",
+ secureRequest: true,
+ refererUntrusted: true,
+ tokenValid: true,
+ },
+ {
+ name: "TLS POST without origin with trusted referer & valid token pass",
+ secureRequest: true,
+ refererTrusted: true,
+ tokenValid: true,
+ want: true,
+ },
+ {
+ name: "TLS POST without origin from _cleartext_ same domain referer with valid token reject",
+ secureRequest: true,
+ refererHTTPDowngrade: true,
+ tokenValid: true,
+ },
+ {
+ name: "TLS POST without origin from relative referer with valid token pass",
+ secureRequest: true,
+ refererRelative: true,
+ tokenValid: true,
+ want: true,
+ },
+ {
+ name: "TLS POST without origin from relative referer with invalid token reject",
+ secureRequest: true,
+ refererRelative: true,
+ tokenInvalid: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var token string
+ var flag bool
+ mux := http.NewServeMux()
+ mux.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
+ token = Token(r)
+ }))
+ mux.Handle("/submit", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ flag = true
+ }))
+ p := Protect(testKey)(mux)
+
+ // Obtain a CSRF cookie via a GET request.
+ r := createRequest("GET", "/", tt.secureRequest)
+ rr := httptest.NewRecorder()
+ p.ServeHTTP(rr, r)
+
+ r = createRequest("POST", "/submit", tt.secureRequest)
+ if tt.safeMethod {
+ r = createRequest("GET", "/submit", tt.secureRequest)
+ }
+
+ // Set the Origin header
+ switch {
+ case tt.originUntrusted:
+ r.Header.Set("Origin", "http://www.untrusted-origin.org")
+ case tt.originTrusted:
+ r.Header.Set("Origin", "https://www.gorillatoolkit.org")
+ case tt.originHTTP:
+ r.Header.Set("Origin", "http://www.gorillatoolkit.org")
+ }
+
+ // Set the Referer header
+ switch {
+ case tt.refererTrusted:
+ p = Protect(testKey, TrustedOrigins([]string{"external-trusted-origin.test"}))(mux)
+ r.Header.Set("Referer", "https://external-trusted-origin.test/foobar")
+ case tt.refererUntrusted:
+ r.Header.Set("Referer", "http://www.invalid-referer.org")
+ case tt.refererHTTPDowngrade:
+ r.Header.Set("Referer", "http://www.gorillatoolkit.org/foobar")
+ case tt.refererRelative:
+ r.Header.Set("Referer", "/foobar")
+ }
+
+ // Set the CSRF token & associated cookie
+ switch {
+ case tt.tokenInvalid:
+ setCookie(rr, r)
+ r.Header.Set("X-CSRF-Token", "this-is-an-invalid-token")
+ case tt.tokenValid:
+ setCookie(rr, r)
+ r.Header.Set("X-CSRF-Token", token)
+ }
+
+ rr = httptest.NewRecorder()
+ p.ServeHTTP(rr, r)
+
+ if tt.want && rr.Code != http.StatusOK {
+ t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
+ rr.Code, http.StatusOK)
+ }
+
+ if tt.want && !flag {
+ t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
+ flag, true)
+
+ }
+ if !tt.want && flag {
+ t.Fatalf("middleware failed to reject the request: got %v want %v", flag, false)
+ }
+ })
+ }
+}
+
+func createRequest(method, path string, useTLS bool) *http.Request {
+ r := httptest.NewRequest(method, path, nil)
+ r.Host = "www.gorillatoolkit.org"
+ if !useTLS {
+ return PlaintextHTTPRequest(r)
+ }
+ return r
+}
diff --git a/doc.go b/doc.go
index 612c8d9..503c948 100644
--- a/doc.go
+++ b/doc.go
@@ -4,13 +4,15 @@ 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.
+* 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:
@@ -31,64 +33,69 @@ 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!
-
-
-
-
-
- `
-
- 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.
- r.HandleFunc("/signup/post", SubmitSignupForm)
-
- // Add the middleware to your router by wrapping it.
- http.ListenAndServe(":8000",
- csrf.Protect([]byte("32-byte-long-auth-key"))(r))
- }
-
- 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)
- }
+ package main
+
+ import (
+ "fmt"
+ "html/template"
+ "net/http"
+
+ "github.com/gorilla/csrf"
+ "github.com/gorilla/mux"
+ )
+
+ var form = `
+
+
+ Sign Up!
+
+
+
+
+
+ `
+
+ 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
@@ -99,59 +106,69 @@ 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
+ package main
- import (
- "github.com/gorilla/csrf"
- "github.com/gorilla/mux"
- )
+ import (
+ "github.com/gorilla/csrf"
+ "github.com/gorilla/mux"
+ )
- func main() {
- r := mux.NewRouter()
+ func main() {
+ r := mux.NewRouter()
- api := r.PathPrefix("/api").Subrouter()
- api.HandleFunc("/user/:id", GetUser).Methods("GET")
+ api := r.PathPrefix("/api").Subrouter()
+ api.HandleFunc("/user/:id", GetUser).Methods("GET")
- http.ListenAndServe(":8000",
- csrf.Protect([]byte("32-byte-long-auth-key"))(r))
- }
+ 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.
+ 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
- }
+ // 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)
- }
+ 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 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.
diff --git a/examples/api-backends/README.md b/examples/api-backends/README.md
new file mode 100644
index 0000000..c2a8eb0
--- /dev/null
+++ b/examples/api-backends/README.md
@@ -0,0 +1,11 @@
+# API Backends
+
+Examples in this directory are intended to provide basic working backend CSRF-protected APIs,
+compatible with the JavaScript frontend examples available in the
+[`examples/javascript-frontends`](../javascript-frontends).
+
+In addition to CSRF protection, these backends provide the CORS configuration required for
+communicating the CSRF cookies and headers with JavaScript client code running in the browser.
+
+See [`examples/javascript-frontends`](../javascript-frontends/README.md) for details on CORS and
+CSRF configuration compatibility requirements.
diff --git a/examples/api-backends/gorilla-mux/go.mod b/examples/api-backends/gorilla-mux/go.mod
new file mode 100644
index 0000000..17e266f
--- /dev/null
+++ b/examples/api-backends/gorilla-mux/go.mod
@@ -0,0 +1,17 @@
+// +build ignore
+
+module github.com/gorilla-mux/examples/api-backends/gorilla-mux
+
+go 1.20
+
+require (
+ github.com/gorilla/csrf v1.7.1
+ github.com/gorilla/handlers v1.5.1
+ github.com/gorilla/mux v1.8.0
+)
+
+require (
+ github.com/felixge/httpsnoop v1.0.3 // indirect
+ github.com/gorilla/securecookie v1.1.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+)
diff --git a/examples/api-backends/gorilla-mux/go.sum b/examples/api-backends/gorilla-mux/go.sum
new file mode 100644
index 0000000..8271f7f
--- /dev/null
+++ b/examples/api-backends/gorilla-mux/go.sum
@@ -0,0 +1,13 @@
+github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
+github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/examples/api-backends/gorilla-mux/main.go b/examples/api-backends/gorilla-mux/main.go
new file mode 100644
index 0000000..849549a
--- /dev/null
+++ b/examples/api-backends/gorilla-mux/main.go
@@ -0,0 +1,66 @@
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/gorilla/csrf"
+ "github.com/gorilla/handlers"
+ "github.com/gorilla/mux"
+)
+
+func main() {
+ router := mux.NewRouter()
+
+ loggingMiddleware := func(h http.Handler) http.Handler {
+ return handlers.LoggingHandler(os.Stdout, h)
+ }
+ router.Use(loggingMiddleware)
+
+ CSRFMiddleware := csrf.Protect(
+ []byte("place-your-32-byte-long-key-here"),
+ csrf.Secure(false), // false in development only!
+ csrf.RequestHeader("X-CSRF-Token"), // Must be in CORS Allowed and Exposed Headers
+ )
+
+ APIRouter := router.PathPrefix("/api").Subrouter()
+ APIRouter.Use(CSRFMiddleware)
+ APIRouter.HandleFunc("", Get).Methods(http.MethodGet)
+ APIRouter.HandleFunc("", Post).Methods(http.MethodPost)
+
+ CORSMiddleware := handlers.CORS(
+ handlers.AllowCredentials(),
+ handlers.AllowedOriginValidator(
+ func(origin string) bool {
+ return strings.HasPrefix(origin, "http://localhost")
+ },
+ ),
+ handlers.AllowedHeaders([]string{"X-CSRF-Token"}),
+ handlers.ExposedHeaders([]string{"X-CSRF-Token"}),
+ )
+
+ server := &http.Server{
+ Handler: CORSMiddleware(router),
+ Addr: "localhost:8080",
+ ReadTimeout: 60 * time.Second,
+ WriteTimeout: 60 * time.Second,
+ }
+
+ fmt.Println("starting http server on localhost:8080")
+ log.Panic(server.ListenAndServe())
+}
+
+func Get(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("X-CSRF-Token", csrf.Token(r))
+ w.WriteHeader(http.StatusOK)
+}
+
+func Post(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/examples/javascript-frontends/README.md b/examples/javascript-frontends/README.md
new file mode 100644
index 0000000..02a1de4
--- /dev/null
+++ b/examples/javascript-frontends/README.md
@@ -0,0 +1,19 @@
+# JavaScript Frontends
+
+Examples in this directory are intended to provide basic working frontend JavaScript, compatible
+with the API backend examples available in the [`examples/api-backends`](../api-backends).
+
+## CSRF and CORS compatibility
+
+In order to be compatible with a CSRF-protected backend, frontend clients must:
+
+1. Be served from a domain allowed by the backend's CORS Allowed Origins configuration.
+ 1. `http://localhost*` for the backend examples provided
+ 2. An example server to serve the HTML and JavaScript for the frontend examples from localhost is included in
+ [`examples/javascript-frontends/example-frontend-server`](../javascript-frontends/example-frontend-server)
+3. Use the HTTP headers expected by the backend to send and receive CSRF Tokens.
+ The backends configure this as the Gorilla `csrf.RequestHeader`,
+ as well as the CORS Allowed Headers and Exposed Headers.
+ 1. `X-CSRF-Token` for the backend examples provided
+ 2. Note that some JavaScript HTTP clients automatically lowercase all received headers,
+ so the values must be accessed with the key `"x-csrf-token"` in the frontend code.
diff --git a/examples/javascript-frontends/example-frontend-server/go.mod b/examples/javascript-frontends/example-frontend-server/go.mod
new file mode 100644
index 0000000..af48442
--- /dev/null
+++ b/examples/javascript-frontends/example-frontend-server/go.mod
@@ -0,0 +1,10 @@
+module github.com/gorilla-mux/examples/javascript-frontends/example-frontend-server
+
+go 1.20
+
+require (
+ github.com/gorilla/handlers v1.5.1
+ github.com/gorilla/mux v1.8.0
+)
+
+require github.com/felixge/httpsnoop v1.0.3 // indirect
diff --git a/examples/javascript-frontends/example-frontend-server/go.sum b/examples/javascript-frontends/example-frontend-server/go.sum
new file mode 100644
index 0000000..1e9fd4d
--- /dev/null
+++ b/examples/javascript-frontends/example-frontend-server/go.sum
@@ -0,0 +1,7 @@
+github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
diff --git a/examples/javascript-frontends/example-frontend-server/main.go b/examples/javascript-frontends/example-frontend-server/main.go
new file mode 100644
index 0000000..6cac20b
--- /dev/null
+++ b/examples/javascript-frontends/example-frontend-server/main.go
@@ -0,0 +1,42 @@
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/gorilla/handlers"
+ "github.com/gorilla/mux"
+)
+
+func main() {
+ router := mux.NewRouter()
+
+ loggingMiddleware := func(h http.Handler) http.Handler {
+ return handlers.LoggingHandler(os.Stdout, h)
+ }
+ router.Use(loggingMiddleware)
+
+ wd, err := os.Getwd()
+ if err != nil {
+ log.Panic(err)
+ }
+ // change this directory to point at a different Javascript frontend to serve
+ httpStaticAssetsDir := http.Dir(fmt.Sprintf("%s/../frontends/axios/", wd))
+
+ router.PathPrefix("/").Handler(http.FileServer(httpStaticAssetsDir))
+
+ server := &http.Server{
+ Handler: router,
+ Addr: "localhost:8081",
+ ReadTimeout: 60 * time.Second,
+ WriteTimeout: 60 * time.Second,
+ }
+
+ fmt.Println("starting http server on localhost:8081")
+ log.Panic(server.ListenAndServe())
+}
diff --git a/examples/javascript-frontends/frontends/axios/index.html b/examples/javascript-frontends/frontends/axios/index.html
new file mode 100644
index 0000000..29ac9c0
--- /dev/null
+++ b/examples/javascript-frontends/frontends/axios/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+ Gorilla CSRF
+
+
+
+
+
Gorilla CSRF: Axios JS Frontend
+
See Console and Network tabs of your browser's Developer Tools for further details
+
+
+
+
Get Request:
+
Full Response:
+
+
CSRF Token:
+
+
+
+
+
+
Post Request:
+
Full Response:
+
+ Note that the X-CSRF-Token value is in the Axios config.headers;
+ it is not a response header set by the server.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/javascript-frontends/frontends/axios/index.js b/examples/javascript-frontends/frontends/axios/index.js
new file mode 100644
index 0000000..d931c58
--- /dev/null
+++ b/examples/javascript-frontends/frontends/axios/index.js
@@ -0,0 +1,50 @@
+// make GET request to backend on page load in order to obtain
+// a CSRF Token and load it into the Axios instance's headers
+// https://github.com/axios/axios#creating-an-instance
+const initializeAxiosInstance = async (url) => {
+ try {
+ let resp = await axios.get(url, {withCredentials: true});
+ console.log(resp);
+ document.getElementById("get-request-full-response").innerHTML = JSON.stringify(resp);
+
+ let csrfToken = parseCSRFToken(resp);
+ console.log(csrfToken);
+ document.getElementById("get-response-csrf-token").innerHTML = csrfToken;
+
+ return axios.create({
+ // withCredentials must be true to in order for the browser
+ // to send cookies, which are necessary for CSRF verification
+ withCredentials: true,
+ headers: {"X-CSRF-Token": csrfToken}
+ });
+ } catch (err) {
+ console.log(err);
+ }
+};
+
+const post = async (axiosInstance, url) => {
+ try {
+ let resp = await axiosInstance.post(url);
+ console.log(resp);
+ document.getElementById("post-request-full-response").innerHTML = JSON.stringify(resp);
+ } catch (err) {
+ console.log(err);
+ }
+};
+
+// general-purpose func to deal with clients like Axios,
+// which lowercase all headers received from the server response
+const parseCSRFToken = (resp) => {
+ let csrfToken = resp.headers[csrfTokenHeader];
+ if (!csrfToken) {
+ csrfToken = resp.headers[csrfTokenHeader.toLowerCase()];
+ }
+ return csrfToken
+}
+
+const url = "http://localhost:8080/api";
+const csrfTokenHeader = "X-CSRF-Token";
+initializeAxiosInstance(url)
+ .then(axiosInstance => {
+ post(axiosInstance, url);
+ });
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..09c4174
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/gorilla/csrf
+
+require github.com/gorilla/securecookie v1.1.2
+
+go 1.20
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..285ffee
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,3 @@
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
diff --git a/helpers.go b/helpers.go
index 3dacfd2..99005ee 100644
--- a/helpers.go
+++ b/helpers.go
@@ -51,18 +51,17 @@ func UnsafeSkipCheck(r *http.Request) *http.Request {
//
// Example:
//
-// // The following tag in our form.tmpl template:
-// {{ .csrfField }}
-//
-// // ... becomes:
-//
+// // 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(fragment) // #nosec G203
}
return template.HTML("")
@@ -75,7 +74,7 @@ func TemplateField(r *http.Request) template.HTML {
// 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 {
+func mask(realToken []byte, _ *http.Request) string {
otp, err := generateRandomBytes(tokenLength)
if err != nil {
return ""
@@ -105,7 +104,7 @@ func unmask(issued []byte) []byte {
// 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 {
+func (cs *csrf) requestToken(r *http.Request) ([]byte, error) {
// 1. Check the HTTP header first.
issued := r.Header.Get(cs.opts.RequestHeader)
@@ -123,14 +122,19 @@ func (cs *csrf) requestToken(r *http.Request) []byte {
}
}
+ // Return nil (equivalent to empty byte slice) if no token was found
+ if issued == "" {
+ return nil, nil
+ }
+
// 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 nil, err
}
- return decoded
+ return decoded, nil
}
// generateRandomBytes returns securely generated random bytes.
diff --git a/helpers_test.go b/helpers_test.go
index 30ee7b3..f40c996 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"fmt"
"io"
+ "log"
"mime/multipart"
"net/http"
"net/http/httptest"
@@ -34,9 +35,12 @@ func TestFormToken(t *testing.T) {
s.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token = Token(r)
t := template.Must((template.New("base").Parse(testTemplate)))
- t.Execute(w, map[string]interface{}{
+ err := t.Execute(w, map[string]interface{}{
TemplateTag: TemplateField(r),
})
+ if err != nil {
+ log.Printf("errored during executing the template: %v", err)
+ }
}))
r, err := http.NewRequest("GET", "/", nil)
@@ -71,15 +75,15 @@ func TestMultipartFormToken(t *testing.T) {
s.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token = Token(r)
t := template.Must((template.New("base").Parse(testTemplate)))
- t.Execute(w, map[string]interface{}{
+ err := t.Execute(w, map[string]interface{}{
TemplateTag: TemplateField(r),
})
+ if err != nil {
+ log.Printf("errored during executing the template: %v", err)
+ }
}))
- r, err := http.NewRequest("GET", "/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("GET", "/", true)
rr := httptest.NewRecorder()
p := Protect(testKey)(s)
@@ -93,16 +97,20 @@ func TestMultipartFormToken(t *testing.T) {
t.Fatal(err)
}
- wr.Write([]byte(token))
- mp.Close()
-
- r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", &b)
+ _, err = wr.Write([]byte(token))
if err != nil {
t.Fatal(err)
}
+ mp.Close()
+
+ r = httptest.NewRequest("POST", "/", &b)
+ r.Host = "www.gorillatoolkit.org"
+
// Add the multipart header.
r.Header.Set("Content-Type", mp.FormDataContentType())
+ // Add Origin to pass the same-origin check.
+ r.Header.Set("Origin", "https://www.gorillatoolkit.org")
// Send back the issued cookie.
setCookie(rr, r)
@@ -185,7 +193,7 @@ func TestXOR(t *testing.T) {
for _, token := range testTokens {
if res := xorToken(token.a, token.b); res != nil {
- if bytes.Compare(res, token.expected) != 0 {
+ if !bytes.Equal(res, token.expected) {
t.Fatalf("xorBytes failed to return the expected result: got %v want %v",
res, token.expected)
}
@@ -226,16 +234,17 @@ func TestTemplateField(t *testing.T) {
token = Token(r)
templateField = string(TemplateField(r))
t := template.Must((template.New("base").Parse(testTemplate)))
- t.Execute(w, map[string]interface{}{
+ err := t.Execute(w, map[string]interface{}{
TemplateTag: TemplateField(r),
})
+ if err != nil {
+ log.Printf("errored during executing the template: %v", err)
+ }
}))
testFieldName := "custom_field_name"
- r, err := http.NewRequest("GET", "/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("GET", "/", false)
+ // r, err := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
p := Protect(testKey, FieldName(testFieldName))(s)
@@ -280,15 +289,12 @@ func TestUnsafeSkipCSRFCheck(t *testing.T) {
var teapot = 418
// Issue a POST request without a CSRF token in the request.
- s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
// Set a non-200 header to make the test explicit.
w.WriteHeader(teapot)
}))
- r, err := http.NewRequest("POST", "/", nil)
- if err != nil {
- t.Fatal(err)
- }
+ r := createRequest("POST", "/", false)
// Must be used prior to the CSRF handler being invoked.
p := skipCheck(Protect(testKey)(s))
diff --git a/options.go b/options.go
index c644d49..c61d301 100644
--- a/options.go
+++ b/options.go
@@ -1,12 +1,15 @@
package csrf
-import "net/http"
+import (
+ "net/http"
+)
// Option describes a functional option for configuring the CSRF handler.
type Option func(*csrf)
// MaxAge sets the maximum age (in seconds) of a CSRF token's underlying cookie.
-// Defaults to 12 hours.
+// Defaults to 12 hours. Call csrf.MaxAge(0) to explicitly set session-only
+// cookies.
func MaxAge(age int) Option {
return func(cs *csrf) {
cs.opts.MaxAge = age
@@ -58,12 +61,32 @@ func HttpOnly(h bool) Option {
}
}
+// SameSite sets the cookie SameSite attribute. Defaults to blank to maintain
+// backwards compatibility, however, Strict is recommended.
+//
+// SameSite(SameSiteStrictMode) will prevent the cookie from being sent by the
+// browser to the target site in all cross-site browsing context, even when
+// following a regular link (GET request).
+//
+// SameSite(SameSiteLaxMode) provides a reasonable balance between security and
+// usability for websites that want to maintain user's logged-in session after
+// the user arrives from an external link. The session cookie would be allowed
+// when following a regular link from an external website while blocking it in
+// CSRF-prone request methods (e.g. POST).
+//
+// This option is only available for go 1.11+.
+func SameSite(s SameSiteMode) Option {
+ return func(cs *csrf) {
+ cs.opts.SameSite = s
+ }
+}
+
// ErrorHandler allows you to change the handler called when CSRF request
// processing encounters an invalid token or request. A typical use would be to
// provide a handler that returns a static HTML file with a HTTP 403 status. By
// 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.Failure(r)
+// 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) {
@@ -97,6 +120,17 @@ func CookieName(name string) Option {
}
}
+// TrustedOrigins configures a set of origins (Referers) that are considered as trusted.
+// This will allow cross-domain CSRF use-cases - e.g. where the front-end is served
+// from a different domain than the API server - to correctly pass a CSRF check.
+//
+// You should only provide origins you own or have full control over.
+func TrustedOrigins(origins []string) Option {
+ return func(cs *csrf) {
+ cs.opts.TrustedOrigins = origins
+ }
+}
+
// setStore sets the store used by the CSRF middleware.
// Note: this is private (for now) to allow for internal API changes.
func setStore(s store) Option {
@@ -118,6 +152,13 @@ func parseOptions(h http.Handler, opts ...Option) *csrf {
cs.opts.Secure = true
cs.opts.HttpOnly = true
+ // Set SameSite=Lax by default, allowing the CSRF cookie to only be sent on
+ // top-level navigations.
+ cs.opts.SameSite = SameSiteLaxMode
+
+ // Default; only override this if the package user explicitly calls MaxAge(0)
+ cs.opts.MaxAge = defaultAge
+
// Range over each options function and apply it
// to our csrf type to configure it. Options functions are
// applied in order, with any conflicting options overriding
diff --git a/options_test.go b/options_test.go
index 13e259c..d133fd1 100644
--- a/options_test.go
+++ b/options_test.go
@@ -24,6 +24,7 @@ func TestOptions(t *testing.T) {
Path(path),
HttpOnly(false),
Secure(false),
+ SameSite(SameSiteStrictMode),
RequestHeader(header),
FieldName(field),
ErrorHandler(http.HandlerFunc(errorHandler)),
@@ -53,6 +54,10 @@ func TestOptions(t *testing.T) {
t.Errorf("Secure not set correctly: got %v want %v", cs.opts.Secure, false)
}
+ if cs.opts.SameSite != SameSiteStrictMode {
+ t.Errorf("SameSite not set correctly: got %v want %v", cs.opts.SameSite, SameSiteStrictMode)
+ }
+
if cs.opts.RequestHeader != header {
t.Errorf("RequestHeader not set correctly: got %v want %v", cs.opts.RequestHeader, header)
}
@@ -71,3 +76,26 @@ func TestOptions(t *testing.T) {
cs.opts.CookieName, name)
}
}
+
+func TestMaxAge(t *testing.T) {
+ t.Run("Ensure the default MaxAge is applied", func(t *testing.T) {
+ handler := Protect(testKey)(nil)
+ csrf := handler.(*csrf)
+ cs := csrf.st.(*cookieStore)
+
+ if cs.maxAge != defaultAge {
+ t.Fatalf("default maxAge not applied: got %d (want %d)", cs.maxAge, defaultAge)
+ }
+ })
+
+ t.Run("Support an explicit MaxAge of 0 (session-only)", func(t *testing.T) {
+ handler := Protect(testKey, MaxAge(0))(nil)
+ csrf := handler.(*csrf)
+ cs := csrf.st.(*cookieStore)
+
+ if cs.maxAge != 0 {
+ t.Fatalf("zero (0) maxAge not applied: got %d (want %d)", cs.maxAge, 0)
+ }
+ })
+
+}
diff --git a/store.go b/store.go
index 39f47ad..4e6382b 100644
--- a/store.go
+++ b/store.go
@@ -1,3 +1,6 @@
+//go:build go1.11
+// +build go1.11
+
package csrf
import (
@@ -28,6 +31,7 @@ type cookieStore struct {
path string
domain string
sc *securecookie.SecureCookie
+ sameSite SameSiteMode
}
// Get retrieves a CSRF token from the session cookie. It returns an empty token
@@ -63,6 +67,7 @@ func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error {
MaxAge: cs.maxAge,
HttpOnly: cs.httpOnly,
Secure: cs.secure,
+ SameSite: http.SameSite(cs.sameSite),
Path: cs.path,
Domain: cs.domain,
}
diff --git a/store_legacy.go b/store_legacy.go
new file mode 100644
index 0000000..4e1fb9e
--- /dev/null
+++ b/store_legacy.go
@@ -0,0 +1,88 @@
+//go:build !go1.11
+// +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/store_legacy_test.go b/store_legacy_test.go
new file mode 100644
index 0000000..ce64a6e
--- /dev/null
+++ b/store_legacy_test.go
@@ -0,0 +1,163 @@
+//go:build !go1.11
+// +build !go1.11
+
+// file for compatibility with go versions prior to 1.11
+
+package csrf
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/gorilla/securecookie"
+)
+
+// Check store implementations
+var _ store = &cookieStore{}
+
+// brokenSaveStore is a CSRF store that cannot, well, save.
+type brokenSaveStore struct {
+ store
+}
+
+func (bs *brokenSaveStore) Get(*http.Request) ([]byte, error) {
+ // Generate an invalid token so we can progress to our Save method
+ return generateRandomBytes(24)
+}
+
+func (bs *brokenSaveStore) Save(realToken []byte, w http.ResponseWriter) error {
+ return errors.New("test error")
+}
+
+// Tests for failure if the middleware can't save to the Store.
+func TestStoreCannotSave(t *testing.T) {
+ s := http.NewServeMux()
+ bs := &brokenSaveStore{}
+ s.HandleFunc("/", testHandler)
+ p := Protect(testKey, setStore(bs))(s)
+
+ r, err := http.NewRequest("GET", "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ p.ServeHTTP(rr, r)
+
+ if rr.Code != http.StatusForbidden {
+ t.Fatalf("broken store did not set an error status: got %v want %v",
+ rr.Code, http.StatusForbidden)
+ }
+
+ if c := rr.Header().Get("Set-Cookie"); c != "" {
+ t.Fatalf("broken store incorrectly set a cookie: got %v want %v",
+ c, "")
+ }
+
+}
+
+// TestCookieDecode tests that an invalid cookie store returns a decoding error.
+func TestCookieDecode(t *testing.T) {
+ r, err := http.NewRequest("GET", "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var age = 3600
+
+ // Test with a nil hash key
+ sc := securecookie.New(nil, nil)
+ sc.MaxAge(age)
+ st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
+
+ // Set a fake cookie value so r.Cookie passes.
+ r.Header.Set("Cookie", fmt.Sprintf("%s=%s", cookieName, "notacookie"))
+
+ _, err = st.Get(r)
+ if err == nil {
+ t.Fatal("cookiestore did not report an invalid hashkey on decode")
+ }
+}
+
+// TestCookieEncode tests that an invalid cookie store returns an encoding error.
+func TestCookieEncode(t *testing.T) {
+ var age = 3600
+
+ // Test with a nil hash key
+ sc := securecookie.New(nil, nil)
+ sc.MaxAge(age)
+ st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
+
+ rr := httptest.NewRecorder()
+
+ err := st.Save(nil, rr)
+ if err == nil {
+ t.Fatal("cookiestore did not report an invalid hashkey on encode")
+ }
+}
+
+// TestMaxAgeZero tests that setting MaxAge(0) does not set the Expires
+// attribute on the cookie.
+func TestMaxAgeZero(t *testing.T) {
+ var age = 0
+
+ s := http.NewServeMux()
+ s.HandleFunc("/", testHandler)
+
+ r, err := http.NewRequest("GET", "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ p := Protect(testKey, MaxAge(age))(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, "Expires") {
+ t.Fatalf("cookie incorrectly has the Expires attribute set: got %q", cookie)
+ }
+}
+
+// TestSameSizeSet tests that setting SameSite Option does not set the SameSite
+// attribute on the cookie in legacy systems.
+func TestSameSizeSet(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, SameSite(SameSiteStrictMode))(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, "SameSite") {
+ t.Fatalf("cookie incorrectly has the SameSite attribute set: got %q", cookie)
+ }
+}
diff --git a/store_test.go b/store_test.go
index e2836e5..712a438 100644
--- a/store_test.go
+++ b/store_test.go
@@ -1,14 +1,16 @@
+//go:build go1.11
+// +build go1.11
+
package csrf
import (
+ "errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
- "github.com/pkg/errors"
-
"github.com/gorilla/securecookie"
)
@@ -17,7 +19,7 @@ var _ store = &cookieStore{}
// brokenSaveStore is a CSRF store that cannot, well, save.
type brokenSaveStore struct {
- store
+ store // nolint:unused
}
func (bs *brokenSaveStore) Get(*http.Request) ([]byte, error) {
@@ -68,7 +70,7 @@ func TestCookieDecode(t *testing.T) {
// Test with a nil hash key
sc := securecookie.New(nil, nil)
sc.MaxAge(age)
- st := &cookieStore{cookieName, age, true, true, "", "", sc}
+ st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
// Set a fake cookie value so r.Cookie passes.
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", cookieName, "notacookie"))
@@ -86,7 +88,7 @@ func TestCookieEncode(t *testing.T) {
// Test with a nil hash key
sc := securecookie.New(nil, nil)
sc.MaxAge(age)
- st := &cookieStore{cookieName, age, true, true, "", "", sc}
+ st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
rr := httptest.NewRecorder()
@@ -127,3 +129,64 @@ func TestMaxAgeZero(t *testing.T) {
t.Fatalf("cookie incorrectly has the Expires attribute set: got %q", cookie)
}
}
+
+// TestSameSizeSet tests that setting SameSite Option sets the SameSite
+// attribute on the cookie in post go1.11 systems.
+func TestSameSizeSet(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, SameSite(SameSiteStrictMode))(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, "SameSite") {
+ t.Fatalf("cookie incorrectly does not have the SameSite attribute set: got %q", cookie)
+ }
+}
+
+// TestSameSiteDefault tests that the default set of options
+// set SameSite=Lax on the CSRF cookie.
+func TestSameSiteDefaultLaxMode(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)
+ }
+
+ cookie := rr.Header().Get("Set-Cookie")
+ if cookie == "" {
+ t.Fatalf("cookie not get Set-Cookie header: got headers %v", rr.Header())
+ }
+
+ sameSiteLax := "SameSite=Lax"
+ if !strings.Contains(cookie, sameSiteLax) {
+ t.Fatalf("cookie should contain %q by default: got %s", sameSiteLax, cookie)
+ }
+}
diff --git a/vendor/github.com/gorilla/securecookie/.editorconfig b/vendor/github.com/gorilla/securecookie/.editorconfig
new file mode 100644
index 0000000..2940ec9
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/.editorconfig
@@ -0,0 +1,20 @@
+; 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
diff --git a/vendor/github.com/gorilla/securecookie/.gitignore b/vendor/github.com/gorilla/securecookie/.gitignore
new file mode 100644
index 0000000..84039fe
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/.gitignore
@@ -0,0 +1 @@
+coverage.coverprofile
diff --git a/vendor/github.com/gorilla/securecookie/LICENSE b/vendor/github.com/gorilla/securecookie/LICENSE
new file mode 100644
index 0000000..bb9d80b
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/LICENSE
@@ -0,0 +1,27 @@
+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.
diff --git a/vendor/github.com/gorilla/securecookie/Makefile b/vendor/github.com/gorilla/securecookie/Makefile
new file mode 100644
index 0000000..2b9008a
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/Makefile
@@ -0,0 +1,39 @@
+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 ./...
+
+.PHONY: fuzz
+fuzz:
+ @echo "##### Running fuzz tests"
+ go test -v -fuzz FuzzEncodeDecode -fuzztime 60s
diff --git a/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md
new file mode 100644
index 0000000..c3b9815
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/README.md
@@ -0,0 +1,144 @@
+# gorilla/securecookie
+
+
+[](https://codecov.io/github/gorilla/securecookie)
+[](https://godoc.org/github.com/gorilla/securecookie)
+[](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()`. 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.
+
+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.
+
+### Key Rotation
+Rotating keys is an important part of any security strategy. The `EncodeMulti` and
+`DecodeMulti` functions allow for multiple keys to be rotated in and out.
+For example, let's take a system that stores keys in a map:
+
+```go
+// keys stored in a map will not be persisted between restarts
+// a more persistent storage should be considered for production applications.
+var cookies = map[string]*securecookie.SecureCookie{
+ "previous": securecookie.New(
+ securecookie.GenerateRandomKey(64),
+ securecookie.GenerateRandomKey(32),
+ ),
+ "current": securecookie.New(
+ securecookie.GenerateRandomKey(64),
+ securecookie.GenerateRandomKey(32),
+ ),
+}
+```
+
+Using the current key to encode new cookies:
+```go
+func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
+ value := map[string]string{
+ "foo": "bar",
+ }
+ if encoded, err := securecookie.EncodeMulti("cookie-name", value, cookies["current"]); err == nil {
+ cookie := &http.Cookie{
+ Name: "cookie-name",
+ Value: encoded,
+ Path: "/",
+ }
+ http.SetCookie(w, cookie)
+ }
+}
+```
+
+Later, decode cookies. Check against all valid keys:
+```go
+func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
+ if cookie, err := r.Cookie("cookie-name"); err == nil {
+ value := make(map[string]string)
+ err = securecookie.DecodeMulti("cookie-name", cookie.Value, &value, cookies["current"], cookies["previous"])
+ if err == nil {
+ fmt.Fprintf(w, "The value of foo is %q", value["foo"])
+ }
+ }
+}
+```
+
+Rotate the keys. This strategy allows previously issued cookies to be valid until the next rotation:
+```go
+func Rotate(newCookie *securecookie.SecureCookie) {
+ cookies["previous"] = cookies["current"]
+ cookies["current"] = newCookie
+}
+```
+
+## License
+
+BSD licensed. See the LICENSE file for details.
diff --git a/vendor/github.com/gorilla/securecookie/doc.go b/vendor/github.com/gorilla/securecookie/doc.go
new file mode 100644
index 0000000..ae89408
--- /dev/null
+++ b/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/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go
new file mode 100644
index 0000000..4d5ea86
--- /dev/null
+++ b/vendor/github.com/gorilla/securecookie/securecookie.go
@@ -0,0 +1,649 @@
+// 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 key 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 len(hashKey) == 0 {
+ 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 "", fmt.Errorf("%s: %d", errEncodedValueTooLong, len(b))
+ }
+ // 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 fmt.Errorf("%s: %d", errValueToDecodeTooLong, len(value))
+ }
+ // 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 ( https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) ) 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
+// ( https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) ) 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.
+//
+// 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.
+//
+// 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/vendor/modules.txt b/vendor/modules.txt
new file mode 100644
index 0000000..6224b61
--- /dev/null
+++ b/vendor/modules.txt
@@ -0,0 +1,3 @@
+# github.com/gorilla/securecookie v1.1.2
+## explicit; go 1.20
+github.com/gorilla/securecookie