├── .gitignore
├── vendor
├── github.com
│ └── gorilla
│ │ └── securecookie
│ │ ├── .gitignore
│ │ ├── .editorconfig
│ │ ├── Makefile
│ │ ├── LICENSE
│ │ ├── doc.go
│ │ ├── README.md
│ │ └── securecookie.go
└── modules.txt
├── go.mod
├── go.sum
├── examples
├── javascript-frontends
│ ├── example-frontend-server
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go
│ ├── frontends
│ │ └── axios
│ │ │ ├── index.html
│ │ │ └── index.js
│ └── README.md
└── api-backends
│ ├── gorilla-mux
│ ├── go.mod
│ ├── go.sum
│ └── main.go
│ └── README.md
├── .editorconfig
├── .github
└── workflows
│ ├── issues.yml
│ ├── verify.yml
│ ├── test.yml
│ └── security.yml
├── context.go
├── Gopkg.lock
├── Gopkg.toml
├── Makefile
├── LICENSE
├── store.go
├── store_legacy.go
├── options_test.go
├── store_legacy_test.go
├── store_test.go
├── options.go
├── doc.go
├── helpers.go
├── helpers_test.go
├── csrf.go
├── README.md
└── csrf_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage.coverprofile
2 |
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/.gitignore:
--------------------------------------------------------------------------------
1 | coverage.coverprofile
2 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gorilla/csrf
2 |
3 | require github.com/gorilla/securecookie v1.1.2
4 |
5 | go 1.20
6 |
--------------------------------------------------------------------------------
/vendor/modules.txt:
--------------------------------------------------------------------------------
1 | # github.com/gorilla/securecookie v1.1.2
2 | ## explicit; go 1.20
3 | github.com/gorilla/securecookie
4 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
2 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
3 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
4 |
--------------------------------------------------------------------------------
/examples/javascript-frontends/example-frontend-server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gorilla-mux/examples/javascript-frontends/example-frontend-server
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/gorilla/handlers v1.5.1
7 | github.com/gorilla/mux v1.8.0
8 | )
9 |
10 | require github.com/felixge/httpsnoop v1.0.3 // indirect
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; https://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | insert_final_newline = true
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}]
13 | indent_style = tab
14 | indent_size = 4
15 |
16 | [*.md]
17 | indent_size = 4
18 | trim_trailing_whitespace = false
19 |
20 | eclint_indent_style = unset
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/.editorconfig:
--------------------------------------------------------------------------------
1 | ; https://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | insert_final_newline = true
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}]
13 | indent_style = tab
14 | indent_size = 4
15 |
16 | [*.md]
17 | indent_size = 4
18 | trim_trailing_whitespace = false
19 |
20 | eclint_indent_style = unset
21 |
--------------------------------------------------------------------------------
/examples/api-backends/gorilla-mux/go.mod:
--------------------------------------------------------------------------------
1 | // +build ignore
2 |
3 | module github.com/gorilla-mux/examples/api-backends/gorilla-mux
4 |
5 | go 1.20
6 |
7 | require (
8 | github.com/gorilla/csrf v1.7.1
9 | github.com/gorilla/handlers v1.5.1
10 | github.com/gorilla/mux v1.8.0
11 | )
12 |
13 | require (
14 | github.com/felixge/httpsnoop v1.0.3 // indirect
15 | github.com/gorilla/securecookie v1.1.1 // indirect
16 | github.com/pkg/errors v0.9.1 // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/.github/workflows/issues.yml:
--------------------------------------------------------------------------------
1 | # Add all the issues created to the project.
2 | name: Add issue or pull request to Project
3 |
4 | on:
5 | issues:
6 | types:
7 | - opened
8 | pull_request_target:
9 | types:
10 | - opened
11 | - reopened
12 |
13 | jobs:
14 | add-to-project:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Add issue to project
18 | uses: actions/add-to-project@v0.5.0
19 | with:
20 | project-url: https://github.com/orgs/gorilla/projects/4
21 | github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }}
22 |
--------------------------------------------------------------------------------
/examples/api-backends/README.md:
--------------------------------------------------------------------------------
1 | # API Backends
2 |
3 | Examples in this directory are intended to provide basic working backend CSRF-protected APIs,
4 | compatible with the JavaScript frontend examples available in the
5 | [`examples/javascript-frontends`](../javascript-frontends).
6 |
7 | In addition to CSRF protection, these backends provide the CORS configuration required for
8 | communicating the CSRF cookies and headers with JavaScript client code running in the browser.
9 |
10 | See [`examples/javascript-frontends`](../javascript-frontends/README.md) for details on CORS and
11 | CSRF configuration compatibility requirements.
12 |
--------------------------------------------------------------------------------
/examples/javascript-frontends/example-frontend-server/go.sum:
--------------------------------------------------------------------------------
1 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
3 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
4 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
5 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
6 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
7 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
8 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | //go:build go1.7
2 | // +build go1.7
3 |
4 | package csrf
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "net/http"
10 | )
11 |
12 | func contextGet(r *http.Request, key string) (interface{}, error) {
13 | val := r.Context().Value(key)
14 | if val == nil {
15 | return nil, fmt.Errorf("no value exists in the context for key %q", key)
16 | }
17 | return val, nil
18 | }
19 |
20 | func contextSave(r *http.Request, key string, val interface{}) *http.Request {
21 | ctx := r.Context()
22 | ctx = context.WithValue(ctx, key, val) // nolint:staticcheck
23 | return r.WithContext(ctx)
24 | }
25 |
26 | func contextClear(r *http.Request) {
27 | // no-op for go1.7+
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yml:
--------------------------------------------------------------------------------
1 | name: Verify
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | permissions:
10 | contents: read
11 | jobs:
12 | lint:
13 | strategy:
14 | matrix:
15 | go: ['1.20','1.21']
16 | fail-fast: true
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout Code
20 | uses: actions/checkout@v3
21 |
22 | - name: Setup Go ${{ matrix.go }}
23 | uses: actions/setup-go@v4
24 | with:
25 | go-version: ${{ matrix.go }}
26 | cache: false
27 |
28 | - name: Run GolangCI-Lint
29 | uses: golangci/golangci-lint-action@v3
30 | with:
31 | version: v1.53
32 | args: --timeout=5m
33 |
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | name = "github.com/gorilla/context"
6 | packages = ["."]
7 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
8 | version = "v1.1"
9 |
10 | [[projects]]
11 | name = "github.com/gorilla/securecookie"
12 | packages = ["."]
13 | revision = "667fe4e3466a040b780561fe9b51a83a3753eefc"
14 | version = "v1.1"
15 |
16 | [[projects]]
17 | name = "github.com/pkg/errors"
18 | packages = ["."]
19 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
20 | version = "v0.8.0"
21 |
22 | [solve-meta]
23 | analyzer-name = "dep"
24 | analyzer-version = 1
25 | inputs-digest = "1695686bc8fa0eb76df9fe8c5ca473686071ddcf795a0595a9465a03e8ac9bef"
26 | solver-name = "gps-cdcl"
27 | solver-version = 1
28 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 |
2 | # Gopkg.toml example
3 | #
4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
5 | # for detailed Gopkg.toml documentation.
6 | #
7 | # required = ["github.com/user/thing/cmd/thing"]
8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
9 | #
10 | # [[constraint]]
11 | # name = "github.com/user/project"
12 | # version = "1.0.0"
13 | #
14 | # [[constraint]]
15 | # name = "github.com/user/project2"
16 | # branch = "dev"
17 | # source = "github.com/myfork/project2"
18 | #
19 | # [[override]]
20 | # name = "github.com/x/y"
21 | # version = "2.4.0"
22 |
23 |
24 | [[constraint]]
25 | name = "github.com/gorilla/context"
26 | version = "1.1.0"
27 |
28 | [[constraint]]
29 | name = "github.com/gorilla/securecookie"
30 | version = "1.1.0"
31 |
32 | [[constraint]]
33 | name = "github.com/pkg/errors"
34 | version = "0.8.0"
35 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | permissions:
10 | contents: read
11 | jobs:
12 | unit:
13 | strategy:
14 | matrix:
15 | go: ['1.20','1.21']
16 | os: [ubuntu-latest, macos-latest, windows-latest]
17 | fail-fast: true
18 | runs-on: ${{ matrix.os }}
19 | steps:
20 | - name: Checkout Code
21 | uses: actions/checkout@v3
22 |
23 | - name: Setup Go ${{ matrix.go }}
24 | uses: actions/setup-go@v4
25 | with:
26 | go-version: ${{ matrix.go }}
27 | cache: false
28 |
29 | - name: Run Tests
30 | run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./...
31 |
32 | - name: Upload coverage to Codecov
33 | uses: codecov/codecov-action@v3
34 | with:
35 | files: ./coverage
36 |
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Security
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | permissions:
10 | contents: read
11 | jobs:
12 | scan:
13 | strategy:
14 | matrix:
15 | go: ['1.20','1.21']
16 | fail-fast: true
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout Code
20 | uses: actions/checkout@v3
21 |
22 | - name: Setup Go ${{ matrix.go }}
23 | uses: actions/setup-go@v4
24 | with:
25 | go-version: ${{ matrix.go }}
26 | cache: false
27 |
28 | - name: Run GoSec
29 | uses: securego/gosec@master
30 | with:
31 | args: -exclude-dir examples ./...
32 |
33 | - name: Run GoVulnCheck
34 | uses: golang/govulncheck-action@v1
35 | with:
36 | go-version-input: ${{ matrix.go }}
37 | go-package: ./...
38 |
--------------------------------------------------------------------------------
/examples/javascript-frontends/frontends/axios/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Gorilla CSRF
6 |
7 |
8 |
9 |
10 |
Gorilla CSRF: Axios JS Frontend
11 |
See Console and Network tabs of your browser's Developer Tools for further details
12 |
13 |
14 |
15 |
Get Request:
16 | Full Response:
17 |
18 | CSRF Token:
19 |
20 |
21 |
22 |
23 |
24 |
Post Request:
25 |
Full Response:
26 |
27 | Note that the X-CSRF-Token value is in the Axios config.headers;
28 | it is not a response header set by the server.
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
2 | GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
3 |
4 | GO_SEC=$(shell which gosec 2> /dev/null || echo '')
5 | GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
6 |
7 | GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
8 | GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
9 |
10 | .PHONY: golangci-lint
11 | golangci-lint:
12 | $(if $(GO_LINT), ,go install $(GO_LINT_URI))
13 | @echo "##### Running golangci-lint"
14 | golangci-lint run -v
15 |
16 | .PHONY: gosec
17 | gosec:
18 | $(if $(GO_SEC), ,go install $(GO_SEC_URI))
19 | @echo "##### Running gosec"
20 | gosec ./...
21 |
22 | .PHONY: govulncheck
23 | govulncheck:
24 | $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
25 | @echo "##### Running govulncheck"
26 | govulncheck ./...
27 |
28 | .PHONY: verify
29 | verify: golangci-lint gosec govulncheck
30 |
31 | .PHONY: test
32 | test:
33 | @echo "##### Running tests"
34 | go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
35 |
--------------------------------------------------------------------------------
/examples/javascript-frontends/example-frontend-server/main.go:
--------------------------------------------------------------------------------
1 | // +build ignore
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "os"
10 | "time"
11 |
12 | "github.com/gorilla/handlers"
13 | "github.com/gorilla/mux"
14 | )
15 |
16 | func main() {
17 | router := mux.NewRouter()
18 |
19 | loggingMiddleware := func(h http.Handler) http.Handler {
20 | return handlers.LoggingHandler(os.Stdout, h)
21 | }
22 | router.Use(loggingMiddleware)
23 |
24 | wd, err := os.Getwd()
25 | if err != nil {
26 | log.Panic(err)
27 | }
28 | // change this directory to point at a different Javascript frontend to serve
29 | httpStaticAssetsDir := http.Dir(fmt.Sprintf("%s/../frontends/axios/", wd))
30 |
31 | router.PathPrefix("/").Handler(http.FileServer(httpStaticAssetsDir))
32 |
33 | server := &http.Server{
34 | Handler: router,
35 | Addr: "localhost:8081",
36 | ReadTimeout: 60 * time.Second,
37 | WriteTimeout: 60 * time.Second,
38 | }
39 |
40 | fmt.Println("starting http server on localhost:8081")
41 | log.Panic(server.ListenAndServe())
42 | }
43 |
--------------------------------------------------------------------------------
/examples/api-backends/gorilla-mux/go.sum:
--------------------------------------------------------------------------------
1 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
3 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
4 | github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
5 | github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
6 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
7 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
8 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
9 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
10 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
11 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
12 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
13 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
14 |
--------------------------------------------------------------------------------
/examples/javascript-frontends/README.md:
--------------------------------------------------------------------------------
1 | # JavaScript Frontends
2 |
3 | Examples in this directory are intended to provide basic working frontend JavaScript, compatible
4 | with the API backend examples available in the [`examples/api-backends`](../api-backends).
5 |
6 | ## CSRF and CORS compatibility
7 |
8 | In order to be compatible with a CSRF-protected backend, frontend clients must:
9 |
10 | 1. Be served from a domain allowed by the backend's CORS Allowed Origins configuration.
11 | 1. `http://localhost*` for the backend examples provided
12 | 2. An example server to serve the HTML and JavaScript for the frontend examples from localhost is included in
13 | [`examples/javascript-frontends/example-frontend-server`](../javascript-frontends/example-frontend-server)
14 | 3. Use the HTTP headers expected by the backend to send and receive CSRF Tokens.
15 | The backends configure this as the Gorilla `csrf.RequestHeader`,
16 | as well as the CORS Allowed Headers and Exposed Headers.
17 | 1. `X-CSRF-Token` for the backend examples provided
18 | 2. Note that some JavaScript HTTP clients automatically lowercase all received headers,
19 | so the values must be accessed with the key `"x-csrf-token"` in the frontend code.
20 |
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/Makefile:
--------------------------------------------------------------------------------
1 | GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
2 | GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
3 |
4 | GO_SEC=$(shell which gosec 2> /dev/null || echo '')
5 | GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
6 |
7 | GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
8 | GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
9 |
10 | .PHONY: golangci-lint
11 | golangci-lint:
12 | $(if $(GO_LINT), ,go install $(GO_LINT_URI))
13 | @echo "##### Running golangci-lint"
14 | golangci-lint run -v
15 |
16 | .PHONY: gosec
17 | gosec:
18 | $(if $(GO_SEC), ,go install $(GO_SEC_URI))
19 | @echo "##### Running gosec"
20 | gosec ./...
21 |
22 | .PHONY: govulncheck
23 | govulncheck:
24 | $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
25 | @echo "##### Running govulncheck"
26 | govulncheck ./...
27 |
28 | .PHONY: verify
29 | verify: golangci-lint gosec govulncheck
30 |
31 | .PHONY: test
32 | test:
33 | @echo "##### Running tests"
34 | go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
35 |
36 | .PHONY: fuzz
37 | fuzz:
38 | @echo "##### Running fuzz tests"
39 | go test -v -fuzz FuzzEncodeDecode -fuzztime 60s
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 The Gorilla Authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google Inc. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 The Gorilla Authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google Inc. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/examples/api-backends/gorilla-mux/main.go:
--------------------------------------------------------------------------------
1 | // +build ignore
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "os"
10 | "strings"
11 | "time"
12 |
13 | "github.com/gorilla/csrf"
14 | "github.com/gorilla/handlers"
15 | "github.com/gorilla/mux"
16 | )
17 |
18 | func main() {
19 | router := mux.NewRouter()
20 |
21 | loggingMiddleware := func(h http.Handler) http.Handler {
22 | return handlers.LoggingHandler(os.Stdout, h)
23 | }
24 | router.Use(loggingMiddleware)
25 |
26 | CSRFMiddleware := csrf.Protect(
27 | []byte("place-your-32-byte-long-key-here"),
28 | csrf.Secure(false), // false in development only!
29 | csrf.RequestHeader("X-CSRF-Token"), // Must be in CORS Allowed and Exposed Headers
30 | )
31 |
32 | APIRouter := router.PathPrefix("/api").Subrouter()
33 | APIRouter.Use(CSRFMiddleware)
34 | APIRouter.HandleFunc("", Get).Methods(http.MethodGet)
35 | APIRouter.HandleFunc("", Post).Methods(http.MethodPost)
36 |
37 | CORSMiddleware := handlers.CORS(
38 | handlers.AllowCredentials(),
39 | handlers.AllowedOriginValidator(
40 | func(origin string) bool {
41 | return strings.HasPrefix(origin, "http://localhost")
42 | },
43 | ),
44 | handlers.AllowedHeaders([]string{"X-CSRF-Token"}),
45 | handlers.ExposedHeaders([]string{"X-CSRF-Token"}),
46 | )
47 |
48 | server := &http.Server{
49 | Handler: CORSMiddleware(router),
50 | Addr: "localhost:8080",
51 | ReadTimeout: 60 * time.Second,
52 | WriteTimeout: 60 * time.Second,
53 | }
54 |
55 | fmt.Println("starting http server on localhost:8080")
56 | log.Panic(server.ListenAndServe())
57 | }
58 |
59 | func Get(w http.ResponseWriter, r *http.Request) {
60 | w.Header().Add("X-CSRF-Token", csrf.Token(r))
61 | w.WriteHeader(http.StatusOK)
62 | }
63 |
64 | func Post(w http.ResponseWriter, r *http.Request) {
65 | w.WriteHeader(http.StatusOK)
66 | }
67 |
--------------------------------------------------------------------------------
/examples/javascript-frontends/frontends/axios/index.js:
--------------------------------------------------------------------------------
1 | // make GET request to backend on page load in order to obtain
2 | // a CSRF Token and load it into the Axios instance's headers
3 | // https://github.com/axios/axios#creating-an-instance
4 | const initializeAxiosInstance = async (url) => {
5 | try {
6 | let resp = await axios.get(url, {withCredentials: true});
7 | console.log(resp);
8 | document.getElementById("get-request-full-response").innerHTML = JSON.stringify(resp);
9 |
10 | let csrfToken = parseCSRFToken(resp);
11 | console.log(csrfToken);
12 | document.getElementById("get-response-csrf-token").innerHTML = csrfToken;
13 |
14 | return axios.create({
15 | // withCredentials must be true to in order for the browser
16 | // to send cookies, which are necessary for CSRF verification
17 | withCredentials: true,
18 | headers: {"X-CSRF-Token": csrfToken}
19 | });
20 | } catch (err) {
21 | console.log(err);
22 | }
23 | };
24 |
25 | const post = async (axiosInstance, url) => {
26 | try {
27 | let resp = await axiosInstance.post(url);
28 | console.log(resp);
29 | document.getElementById("post-request-full-response").innerHTML = JSON.stringify(resp);
30 | } catch (err) {
31 | console.log(err);
32 | }
33 | };
34 |
35 | // general-purpose func to deal with clients like Axios,
36 | // which lowercase all headers received from the server response
37 | const parseCSRFToken = (resp) => {
38 | let csrfToken = resp.headers[csrfTokenHeader];
39 | if (!csrfToken) {
40 | csrfToken = resp.headers[csrfTokenHeader.toLowerCase()];
41 | }
42 | return csrfToken
43 | }
44 |
45 | const url = "http://localhost:8080/api";
46 | const csrfTokenHeader = "X-CSRF-Token";
47 | initializeAxiosInstance(url)
48 | .then(axiosInstance => {
49 | post(axiosInstance, url);
50 | });
51 |
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2012 The Gorilla Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | /*
6 | Package securecookie encodes and decodes authenticated and optionally
7 | encrypted cookie values.
8 |
9 | Secure cookies can't be forged, because their values are validated using HMAC.
10 | When encrypted, the content is also inaccessible to malicious eyes.
11 |
12 | To use it, first create a new SecureCookie instance:
13 |
14 | var hashKey = []byte("very-secret")
15 | var blockKey = []byte("a-lot-secret")
16 | var s = securecookie.New(hashKey, blockKey)
17 |
18 | The hashKey is required, used to authenticate the cookie value using HMAC.
19 | It is recommended to use a key with 32 or 64 bytes.
20 |
21 | The blockKey is optional, used to encrypt the cookie value -- set it to nil
22 | to not use encryption. If set, the length must correspond to the block size
23 | of the encryption algorithm. For AES, used by default, valid lengths are
24 | 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
25 |
26 | Strong keys can be created using the convenience function GenerateRandomKey().
27 |
28 | Once a SecureCookie instance is set, use it to encode a cookie value:
29 |
30 | func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
31 | value := map[string]string{
32 | "foo": "bar",
33 | }
34 | if encoded, err := s.Encode("cookie-name", value); err == nil {
35 | cookie := &http.Cookie{
36 | Name: "cookie-name",
37 | Value: encoded,
38 | Path: "/",
39 | }
40 | http.SetCookie(w, cookie)
41 | }
42 | }
43 |
44 | Later, use the same SecureCookie instance to decode and validate a cookie
45 | value:
46 |
47 | func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
48 | if cookie, err := r.Cookie("cookie-name"); err == nil {
49 | value := make(map[string]string)
50 | if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
51 | fmt.Fprintf(w, "The value of foo is %q", value["foo"])
52 | }
53 | }
54 | }
55 |
56 | We stored a map[string]string, but secure cookies can hold any value that
57 | can be encoded using encoding/gob. To store custom types, they must be
58 | registered first using gob.Register(). For basic types this is not needed;
59 | it works out of the box.
60 | */
61 | package securecookie
62 |
--------------------------------------------------------------------------------
/store.go:
--------------------------------------------------------------------------------
1 | //go:build go1.11
2 | // +build go1.11
3 |
4 | package csrf
5 |
6 | import (
7 | "net/http"
8 | "time"
9 |
10 | "github.com/gorilla/securecookie"
11 | )
12 |
13 | // store represents the session storage used for CSRF tokens.
14 | type store interface {
15 | // Get returns the real CSRF token from the store.
16 | Get(*http.Request) ([]byte, error)
17 | // Save stores the real CSRF token in the store and writes a
18 | // cookie to the http.ResponseWriter.
19 | // For non-cookie stores, the cookie should contain a unique (256 bit) ID
20 | // or key that references the token in the backend store.
21 | // csrf.GenerateRandomBytes is a helper function for generating secure IDs.
22 | Save(token []byte, w http.ResponseWriter) error
23 | }
24 |
25 | // cookieStore is a signed cookie session store for CSRF tokens.
26 | type cookieStore struct {
27 | name string
28 | maxAge int
29 | secure bool
30 | httpOnly bool
31 | path string
32 | domain string
33 | sc *securecookie.SecureCookie
34 | sameSite SameSiteMode
35 | }
36 |
37 | // Get retrieves a CSRF token from the session cookie. It returns an empty token
38 | // if decoding fails (e.g. HMAC validation fails or the named cookie doesn't exist).
39 | func (cs *cookieStore) Get(r *http.Request) ([]byte, error) {
40 | // Retrieve the cookie from the request
41 | cookie, err := r.Cookie(cs.name)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | token := make([]byte, tokenLength)
47 | // Decode the HMAC authenticated cookie.
48 | err = cs.sc.Decode(cs.name, cookie.Value, &token)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | return token, nil
54 | }
55 |
56 | // Save stores the CSRF token in the session cookie.
57 | func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error {
58 | // Generate an encoded cookie value with the CSRF token.
59 | encoded, err := cs.sc.Encode(cs.name, token)
60 | if err != nil {
61 | return err
62 | }
63 |
64 | cookie := &http.Cookie{
65 | Name: cs.name,
66 | Value: encoded,
67 | MaxAge: cs.maxAge,
68 | HttpOnly: cs.httpOnly,
69 | Secure: cs.secure,
70 | SameSite: http.SameSite(cs.sameSite),
71 | Path: cs.path,
72 | Domain: cs.domain,
73 | }
74 |
75 | // Set the Expires field on the cookie based on the MaxAge
76 | // If MaxAge <= 0, we don't set the Expires attribute, making the cookie
77 | // session-only.
78 | if cs.maxAge > 0 {
79 | cookie.Expires = time.Now().Add(
80 | time.Duration(cs.maxAge) * time.Second)
81 | }
82 |
83 | // Write the authenticated cookie to the response.
84 | http.SetCookie(w, cookie)
85 |
86 | return nil
87 | }
88 |
--------------------------------------------------------------------------------
/store_legacy.go:
--------------------------------------------------------------------------------
1 | //go:build !go1.11
2 | // +build !go1.11
3 |
4 | // file for compatibility with go versions prior to 1.11
5 |
6 | package csrf
7 |
8 | import (
9 | "net/http"
10 | "time"
11 |
12 | "github.com/gorilla/securecookie"
13 | )
14 |
15 | // store represents the session storage used for CSRF tokens.
16 | type store interface {
17 | // Get returns the real CSRF token from the store.
18 | Get(*http.Request) ([]byte, error)
19 | // Save stores the real CSRF token in the store and writes a
20 | // cookie to the http.ResponseWriter.
21 | // For non-cookie stores, the cookie should contain a unique (256 bit) ID
22 | // or key that references the token in the backend store.
23 | // csrf.GenerateRandomBytes is a helper function for generating secure IDs.
24 | Save(token []byte, w http.ResponseWriter) error
25 | }
26 |
27 | // cookieStore is a signed cookie session store for CSRF tokens.
28 | type cookieStore struct {
29 | name string
30 | maxAge int
31 | secure bool
32 | httpOnly bool
33 | path string
34 | domain string
35 | sc *securecookie.SecureCookie
36 | sameSite SameSiteMode
37 | }
38 |
39 | // Get retrieves a CSRF token from the session cookie. It returns an empty token
40 | // if decoding fails (e.g. HMAC validation fails or the named cookie doesn't exist).
41 | func (cs *cookieStore) Get(r *http.Request) ([]byte, error) {
42 | // Retrieve the cookie from the request
43 | cookie, err := r.Cookie(cs.name)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | token := make([]byte, tokenLength)
49 | // Decode the HMAC authenticated cookie.
50 | err = cs.sc.Decode(cs.name, cookie.Value, &token)
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | return token, nil
56 | }
57 |
58 | // Save stores the CSRF token in the session cookie.
59 | func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error {
60 | // Generate an encoded cookie value with the CSRF token.
61 | encoded, err := cs.sc.Encode(cs.name, token)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | cookie := &http.Cookie{
67 | Name: cs.name,
68 | Value: encoded,
69 | MaxAge: cs.maxAge,
70 | HttpOnly: cs.httpOnly,
71 | Secure: cs.secure,
72 | Path: cs.path,
73 | Domain: cs.domain,
74 | }
75 |
76 | // Set the Expires field on the cookie based on the MaxAge
77 | // If MaxAge <= 0, we don't set the Expires attribute, making the cookie
78 | // session-only.
79 | if cs.maxAge > 0 {
80 | cookie.Expires = time.Now().Add(
81 | time.Duration(cs.maxAge) * time.Second)
82 | }
83 |
84 | // Write the authenticated cookie to the response.
85 | http.SetCookie(w, cookie)
86 |
87 | return nil
88 | }
89 |
--------------------------------------------------------------------------------
/options_test.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "net/http"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | // Tests that options functions are applied to the middleware.
10 | func TestOptions(t *testing.T) {
11 | var h http.Handler
12 |
13 | age := 86400
14 | domain := "gorillatoolkit.org"
15 | path := "/forms/"
16 | header := "X-AUTH-TOKEN"
17 | field := "authenticity_token"
18 | errorHandler := unauthorizedHandler
19 | name := "_chimpanzee_csrf"
20 |
21 | testOpts := []Option{
22 | MaxAge(age),
23 | Domain(domain),
24 | Path(path),
25 | HttpOnly(false),
26 | Secure(false),
27 | SameSite(SameSiteStrictMode),
28 | RequestHeader(header),
29 | FieldName(field),
30 | ErrorHandler(http.HandlerFunc(errorHandler)),
31 | CookieName(name),
32 | }
33 |
34 | // Parse our test options and check that they set the related struct fields.
35 | cs := parseOptions(h, testOpts...)
36 |
37 | if cs.opts.MaxAge != age {
38 | t.Errorf("MaxAge not set correctly: got %v want %v", cs.opts.MaxAge, age)
39 | }
40 |
41 | if cs.opts.Domain != domain {
42 | t.Errorf("Domain not set correctly: got %v want %v", cs.opts.Domain, domain)
43 | }
44 |
45 | if cs.opts.Path != path {
46 | t.Errorf("Path not set correctly: got %v want %v", cs.opts.Path, path)
47 | }
48 |
49 | if cs.opts.HttpOnly != false {
50 | t.Errorf("HttpOnly not set correctly: got %v want %v", cs.opts.HttpOnly, false)
51 | }
52 |
53 | if cs.opts.Secure != false {
54 | t.Errorf("Secure not set correctly: got %v want %v", cs.opts.Secure, false)
55 | }
56 |
57 | if cs.opts.SameSite != SameSiteStrictMode {
58 | t.Errorf("SameSite not set correctly: got %v want %v", cs.opts.SameSite, SameSiteStrictMode)
59 | }
60 |
61 | if cs.opts.RequestHeader != header {
62 | t.Errorf("RequestHeader not set correctly: got %v want %v", cs.opts.RequestHeader, header)
63 | }
64 |
65 | if cs.opts.FieldName != field {
66 | t.Errorf("FieldName not set correctly: got %v want %v", cs.opts.FieldName, field)
67 | }
68 |
69 | if !reflect.ValueOf(cs.opts.ErrorHandler).IsValid() {
70 | t.Errorf("ErrorHandler not set correctly: got %v want %v",
71 | reflect.ValueOf(cs.opts.ErrorHandler).IsValid(), reflect.ValueOf(errorHandler).IsValid())
72 | }
73 |
74 | if cs.opts.CookieName != name {
75 | t.Errorf("CookieName not set correctly: got %v want %v",
76 | cs.opts.CookieName, name)
77 | }
78 | }
79 |
80 | func TestMaxAge(t *testing.T) {
81 | t.Run("Ensure the default MaxAge is applied", func(t *testing.T) {
82 | handler := Protect(testKey)(nil)
83 | csrf := handler.(*csrf)
84 | cs := csrf.st.(*cookieStore)
85 |
86 | if cs.maxAge != defaultAge {
87 | t.Fatalf("default maxAge not applied: got %d (want %d)", cs.maxAge, defaultAge)
88 | }
89 | })
90 |
91 | t.Run("Support an explicit MaxAge of 0 (session-only)", func(t *testing.T) {
92 | handler := Protect(testKey, MaxAge(0))(nil)
93 | csrf := handler.(*csrf)
94 | cs := csrf.st.(*cookieStore)
95 |
96 | if cs.maxAge != 0 {
97 | t.Fatalf("zero (0) maxAge not applied: got %d (want %d)", cs.maxAge, 0)
98 | }
99 | })
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/store_legacy_test.go:
--------------------------------------------------------------------------------
1 | //go:build !go1.11
2 | // +build !go1.11
3 |
4 | // file for compatibility with go versions prior to 1.11
5 |
6 | package csrf
7 |
8 | import (
9 | "errors"
10 | "fmt"
11 | "net/http"
12 | "net/http/httptest"
13 | "strings"
14 | "testing"
15 |
16 | "github.com/gorilla/securecookie"
17 | )
18 |
19 | // Check store implementations
20 | var _ store = &cookieStore{}
21 |
22 | // brokenSaveStore is a CSRF store that cannot, well, save.
23 | type brokenSaveStore struct {
24 | store
25 | }
26 |
27 | func (bs *brokenSaveStore) Get(*http.Request) ([]byte, error) {
28 | // Generate an invalid token so we can progress to our Save method
29 | return generateRandomBytes(24)
30 | }
31 |
32 | func (bs *brokenSaveStore) Save(realToken []byte, w http.ResponseWriter) error {
33 | return errors.New("test error")
34 | }
35 |
36 | // Tests for failure if the middleware can't save to the Store.
37 | func TestStoreCannotSave(t *testing.T) {
38 | s := http.NewServeMux()
39 | bs := &brokenSaveStore{}
40 | s.HandleFunc("/", testHandler)
41 | p := Protect(testKey, setStore(bs))(s)
42 |
43 | r, err := http.NewRequest("GET", "/", nil)
44 | if err != nil {
45 | t.Fatal(err)
46 | }
47 |
48 | rr := httptest.NewRecorder()
49 | p.ServeHTTP(rr, r)
50 |
51 | if rr.Code != http.StatusForbidden {
52 | t.Fatalf("broken store did not set an error status: got %v want %v",
53 | rr.Code, http.StatusForbidden)
54 | }
55 |
56 | if c := rr.Header().Get("Set-Cookie"); c != "" {
57 | t.Fatalf("broken store incorrectly set a cookie: got %v want %v",
58 | c, "")
59 | }
60 |
61 | }
62 |
63 | // TestCookieDecode tests that an invalid cookie store returns a decoding error.
64 | func TestCookieDecode(t *testing.T) {
65 | r, err := http.NewRequest("GET", "/", nil)
66 | if err != nil {
67 | t.Fatal(err)
68 | }
69 |
70 | var age = 3600
71 |
72 | // Test with a nil hash key
73 | sc := securecookie.New(nil, nil)
74 | sc.MaxAge(age)
75 | st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
76 |
77 | // Set a fake cookie value so r.Cookie passes.
78 | r.Header.Set("Cookie", fmt.Sprintf("%s=%s", cookieName, "notacookie"))
79 |
80 | _, err = st.Get(r)
81 | if err == nil {
82 | t.Fatal("cookiestore did not report an invalid hashkey on decode")
83 | }
84 | }
85 |
86 | // TestCookieEncode tests that an invalid cookie store returns an encoding error.
87 | func TestCookieEncode(t *testing.T) {
88 | var age = 3600
89 |
90 | // Test with a nil hash key
91 | sc := securecookie.New(nil, nil)
92 | sc.MaxAge(age)
93 | st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
94 |
95 | rr := httptest.NewRecorder()
96 |
97 | err := st.Save(nil, rr)
98 | if err == nil {
99 | t.Fatal("cookiestore did not report an invalid hashkey on encode")
100 | }
101 | }
102 |
103 | // TestMaxAgeZero tests that setting MaxAge(0) does not set the Expires
104 | // attribute on the cookie.
105 | func TestMaxAgeZero(t *testing.T) {
106 | var age = 0
107 |
108 | s := http.NewServeMux()
109 | s.HandleFunc("/", testHandler)
110 |
111 | r, err := http.NewRequest("GET", "/", nil)
112 | if err != nil {
113 | t.Fatal(err)
114 | }
115 |
116 | rr := httptest.NewRecorder()
117 | p := Protect(testKey, MaxAge(age))(s)
118 | p.ServeHTTP(rr, r)
119 |
120 | if rr.Code != http.StatusOK {
121 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
122 | rr.Code, http.StatusOK)
123 | }
124 |
125 | if rr.Header().Get("Set-Cookie") == "" {
126 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
127 | }
128 |
129 | cookie := rr.Header().Get("Set-Cookie")
130 | if !strings.Contains(cookie, "HttpOnly") || strings.Contains(cookie, "Expires") {
131 | t.Fatalf("cookie incorrectly has the Expires attribute set: got %q", cookie)
132 | }
133 | }
134 |
135 | // TestSameSizeSet tests that setting SameSite Option does not set the SameSite
136 | // attribute on the cookie in legacy systems.
137 | func TestSameSizeSet(t *testing.T) {
138 | s := http.NewServeMux()
139 | s.HandleFunc("/", testHandler)
140 |
141 | r, err := http.NewRequest("GET", "/", nil)
142 | if err != nil {
143 | t.Fatal(err)
144 | }
145 |
146 | rr := httptest.NewRecorder()
147 | p := Protect(testKey, SameSite(SameSiteStrictMode))(s)
148 | p.ServeHTTP(rr, r)
149 |
150 | if rr.Code != http.StatusOK {
151 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
152 | rr.Code, http.StatusOK)
153 | }
154 |
155 | if rr.Header().Get("Set-Cookie") == "" {
156 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
157 | }
158 |
159 | cookie := rr.Header().Get("Set-Cookie")
160 | if strings.Contains(cookie, "SameSite") {
161 | t.Fatalf("cookie incorrectly has the SameSite attribute set: got %q", cookie)
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/README.md:
--------------------------------------------------------------------------------
1 | # gorilla/securecookie
2 |
3 | 
4 | [](https://codecov.io/github/gorilla/securecookie)
5 | [](https://godoc.org/github.com/gorilla/securecookie)
6 | [](https://sourcegraph.com/github.com/gorilla/securecookie?badge)
7 |
8 | 
9 |
10 | securecookie encodes and decodes authenticated and optionally encrypted
11 | cookie values.
12 |
13 | Secure cookies can't be forged, because their values are validated using HMAC.
14 | When encrypted, the content is also inaccessible to malicious eyes. It is still
15 | recommended that sensitive data not be stored in cookies, and that HTTPS be used
16 | to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack).
17 |
18 | ## Examples
19 |
20 | To use it, first create a new SecureCookie instance:
21 |
22 | ```go
23 | // Hash keys should be at least 32 bytes long
24 | var hashKey = []byte("very-secret")
25 | // Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
26 | // Shorter keys may weaken the encryption used.
27 | var blockKey = []byte("a-lot-secret")
28 | var s = securecookie.New(hashKey, blockKey)
29 | ```
30 |
31 | The hashKey is required, used to authenticate the cookie value using HMAC.
32 | It is recommended to use a key with 32 or 64 bytes.
33 |
34 | The blockKey is optional, used to encrypt the cookie value -- set it to nil
35 | to not use encryption. If set, the length must correspond to the block size
36 | of the encryption algorithm. For AES, used by default, valid lengths are
37 | 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
38 |
39 | Strong keys can be created using the convenience function
40 | `GenerateRandomKey()`. Note that keys created using `GenerateRandomKey()` are not
41 | automatically persisted. New keys will be created when the application is
42 | restarted, and previously issued cookies will not be able to be decoded.
43 |
44 | Once a SecureCookie instance is set, use it to encode a cookie value:
45 |
46 | ```go
47 | func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
48 | value := map[string]string{
49 | "foo": "bar",
50 | }
51 | if encoded, err := s.Encode("cookie-name", value); err == nil {
52 | cookie := &http.Cookie{
53 | Name: "cookie-name",
54 | Value: encoded,
55 | Path: "/",
56 | Secure: true,
57 | HttpOnly: true,
58 | }
59 | http.SetCookie(w, cookie)
60 | }
61 | }
62 | ```
63 |
64 | Later, use the same SecureCookie instance to decode and validate a cookie
65 | value:
66 |
67 | ```go
68 | func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
69 | if cookie, err := r.Cookie("cookie-name"); err == nil {
70 | value := make(map[string]string)
71 | if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
72 | fmt.Fprintf(w, "The value of foo is %q", value["foo"])
73 | }
74 | }
75 | }
76 | ```
77 |
78 | We stored a map[string]string, but secure cookies can hold any value that
79 | can be encoded using `encoding/gob`. To store custom types, they must be
80 | registered first using gob.Register(). For basic types this is not needed;
81 | it works out of the box. An optional JSON encoder that uses `encoding/json` is
82 | available for types compatible with JSON.
83 |
84 | ### Key Rotation
85 | Rotating keys is an important part of any security strategy. The `EncodeMulti` and
86 | `DecodeMulti` functions allow for multiple keys to be rotated in and out.
87 | For example, let's take a system that stores keys in a map:
88 |
89 | ```go
90 | // keys stored in a map will not be persisted between restarts
91 | // a more persistent storage should be considered for production applications.
92 | var cookies = map[string]*securecookie.SecureCookie{
93 | "previous": securecookie.New(
94 | securecookie.GenerateRandomKey(64),
95 | securecookie.GenerateRandomKey(32),
96 | ),
97 | "current": securecookie.New(
98 | securecookie.GenerateRandomKey(64),
99 | securecookie.GenerateRandomKey(32),
100 | ),
101 | }
102 | ```
103 |
104 | Using the current key to encode new cookies:
105 | ```go
106 | func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
107 | value := map[string]string{
108 | "foo": "bar",
109 | }
110 | if encoded, err := securecookie.EncodeMulti("cookie-name", value, cookies["current"]); err == nil {
111 | cookie := &http.Cookie{
112 | Name: "cookie-name",
113 | Value: encoded,
114 | Path: "/",
115 | }
116 | http.SetCookie(w, cookie)
117 | }
118 | }
119 | ```
120 |
121 | Later, decode cookies. Check against all valid keys:
122 | ```go
123 | func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
124 | if cookie, err := r.Cookie("cookie-name"); err == nil {
125 | value := make(map[string]string)
126 | err = securecookie.DecodeMulti("cookie-name", cookie.Value, &value, cookies["current"], cookies["previous"])
127 | if err == nil {
128 | fmt.Fprintf(w, "The value of foo is %q", value["foo"])
129 | }
130 | }
131 | }
132 | ```
133 |
134 | Rotate the keys. This strategy allows previously issued cookies to be valid until the next rotation:
135 | ```go
136 | func Rotate(newCookie *securecookie.SecureCookie) {
137 | cookies["previous"] = cookies["current"]
138 | cookies["current"] = newCookie
139 | }
140 | ```
141 |
142 | ## License
143 |
144 | BSD licensed. See the LICENSE file for details.
145 |
--------------------------------------------------------------------------------
/store_test.go:
--------------------------------------------------------------------------------
1 | //go:build go1.11
2 | // +build go1.11
3 |
4 | package csrf
5 |
6 | import (
7 | "errors"
8 | "fmt"
9 | "net/http"
10 | "net/http/httptest"
11 | "strings"
12 | "testing"
13 |
14 | "github.com/gorilla/securecookie"
15 | )
16 |
17 | // Check store implementations
18 | var _ store = &cookieStore{}
19 |
20 | // brokenSaveStore is a CSRF store that cannot, well, save.
21 | type brokenSaveStore struct {
22 | store // nolint:unused
23 | }
24 |
25 | func (bs *brokenSaveStore) Get(*http.Request) ([]byte, error) {
26 | // Generate an invalid token so we can progress to our Save method
27 | return generateRandomBytes(24)
28 | }
29 |
30 | func (bs *brokenSaveStore) Save(realToken []byte, w http.ResponseWriter) error {
31 | return errors.New("test error")
32 | }
33 |
34 | // Tests for failure if the middleware can't save to the Store.
35 | func TestStoreCannotSave(t *testing.T) {
36 | s := http.NewServeMux()
37 | bs := &brokenSaveStore{}
38 | s.HandleFunc("/", testHandler)
39 | p := Protect(testKey, setStore(bs))(s)
40 |
41 | r, err := http.NewRequest("GET", "/", nil)
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 |
46 | rr := httptest.NewRecorder()
47 | p.ServeHTTP(rr, r)
48 |
49 | if rr.Code != http.StatusForbidden {
50 | t.Fatalf("broken store did not set an error status: got %v want %v",
51 | rr.Code, http.StatusForbidden)
52 | }
53 |
54 | if c := rr.Header().Get("Set-Cookie"); c != "" {
55 | t.Fatalf("broken store incorrectly set a cookie: got %v want %v",
56 | c, "")
57 | }
58 |
59 | }
60 |
61 | // TestCookieDecode tests that an invalid cookie store returns a decoding error.
62 | func TestCookieDecode(t *testing.T) {
63 | r, err := http.NewRequest("GET", "/", nil)
64 | if err != nil {
65 | t.Fatal(err)
66 | }
67 |
68 | var age = 3600
69 |
70 | // Test with a nil hash key
71 | sc := securecookie.New(nil, nil)
72 | sc.MaxAge(age)
73 | st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
74 |
75 | // Set a fake cookie value so r.Cookie passes.
76 | r.Header.Set("Cookie", fmt.Sprintf("%s=%s", cookieName, "notacookie"))
77 |
78 | _, err = st.Get(r)
79 | if err == nil {
80 | t.Fatal("cookiestore did not report an invalid hashkey on decode")
81 | }
82 | }
83 |
84 | // TestCookieEncode tests that an invalid cookie store returns an encoding error.
85 | func TestCookieEncode(t *testing.T) {
86 | var age = 3600
87 |
88 | // Test with a nil hash key
89 | sc := securecookie.New(nil, nil)
90 | sc.MaxAge(age)
91 | st := &cookieStore{cookieName, age, true, true, "", "", sc, SameSiteDefaultMode}
92 |
93 | rr := httptest.NewRecorder()
94 |
95 | err := st.Save(nil, rr)
96 | if err == nil {
97 | t.Fatal("cookiestore did not report an invalid hashkey on encode")
98 | }
99 | }
100 |
101 | // TestMaxAgeZero tests that setting MaxAge(0) does not set the Expires
102 | // attribute on the cookie.
103 | func TestMaxAgeZero(t *testing.T) {
104 | var age = 0
105 |
106 | s := http.NewServeMux()
107 | s.HandleFunc("/", testHandler)
108 |
109 | r, err := http.NewRequest("GET", "/", nil)
110 | if err != nil {
111 | t.Fatal(err)
112 | }
113 |
114 | rr := httptest.NewRecorder()
115 | p := Protect(testKey, MaxAge(age))(s)
116 | p.ServeHTTP(rr, r)
117 |
118 | if rr.Code != http.StatusOK {
119 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
120 | rr.Code, http.StatusOK)
121 | }
122 |
123 | if rr.Header().Get("Set-Cookie") == "" {
124 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
125 | }
126 |
127 | cookie := rr.Header().Get("Set-Cookie")
128 | if !strings.Contains(cookie, "HttpOnly") || strings.Contains(cookie, "Expires") {
129 | t.Fatalf("cookie incorrectly has the Expires attribute set: got %q", cookie)
130 | }
131 | }
132 |
133 | // TestSameSizeSet tests that setting SameSite Option sets the SameSite
134 | // attribute on the cookie in post go1.11 systems.
135 | func TestSameSizeSet(t *testing.T) {
136 | s := http.NewServeMux()
137 | s.HandleFunc("/", testHandler)
138 |
139 | r, err := http.NewRequest("GET", "/", nil)
140 | if err != nil {
141 | t.Fatal(err)
142 | }
143 |
144 | rr := httptest.NewRecorder()
145 | p := Protect(testKey, SameSite(SameSiteStrictMode))(s)
146 | p.ServeHTTP(rr, r)
147 |
148 | if rr.Code != http.StatusOK {
149 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
150 | rr.Code, http.StatusOK)
151 | }
152 |
153 | if rr.Header().Get("Set-Cookie") == "" {
154 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
155 | }
156 |
157 | cookie := rr.Header().Get("Set-Cookie")
158 | if !strings.Contains(cookie, "SameSite") {
159 | t.Fatalf("cookie incorrectly does not have the SameSite attribute set: got %q", cookie)
160 | }
161 | }
162 |
163 | // TestSameSiteDefault tests that the default set of options
164 | // set SameSite=Lax on the CSRF cookie.
165 | func TestSameSiteDefaultLaxMode(t *testing.T) {
166 | s := http.NewServeMux()
167 | s.HandleFunc("/", testHandler)
168 |
169 | r, err := http.NewRequest("GET", "/", nil)
170 | if err != nil {
171 | t.Fatal(err)
172 | }
173 |
174 | rr := httptest.NewRecorder()
175 | p := Protect(testKey)(s)
176 | p.ServeHTTP(rr, r)
177 |
178 | if rr.Code != http.StatusOK {
179 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
180 | rr.Code, http.StatusOK)
181 | }
182 |
183 | cookie := rr.Header().Get("Set-Cookie")
184 | if cookie == "" {
185 | t.Fatalf("cookie not get Set-Cookie header: got headers %v", rr.Header())
186 | }
187 |
188 | sameSiteLax := "SameSite=Lax"
189 | if !strings.Contains(cookie, sameSiteLax) {
190 | t.Fatalf("cookie should contain %q by default: got %s", sameSiteLax, cookie)
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | // Option describes a functional option for configuring the CSRF handler.
8 | type Option func(*csrf)
9 |
10 | // MaxAge sets the maximum age (in seconds) of a CSRF token's underlying cookie.
11 | // Defaults to 12 hours. Call csrf.MaxAge(0) to explicitly set session-only
12 | // cookies.
13 | func MaxAge(age int) Option {
14 | return func(cs *csrf) {
15 | cs.opts.MaxAge = age
16 | }
17 | }
18 |
19 | // Domain sets the cookie domain. Defaults to the current domain of the request
20 | // only (recommended).
21 | //
22 | // This should be a hostname and not a URL. If set, the domain is treated as
23 | // being prefixed with a '.' - e.g. "example.com" becomes ".example.com" and
24 | // matches "www.example.com" and "secure.example.com".
25 | func Domain(domain string) Option {
26 | return func(cs *csrf) {
27 | cs.opts.Domain = domain
28 | }
29 | }
30 |
31 | // Path sets the cookie path. Defaults to the path the cookie was issued from
32 | // (recommended).
33 | //
34 | // This instructs clients to only respond with cookie for that path and its
35 | // subpaths - i.e. a cookie issued from "/register" would be included in requests
36 | // to "/register/step2" and "/register/submit".
37 | func Path(p string) Option {
38 | return func(cs *csrf) {
39 | cs.opts.Path = p
40 | }
41 | }
42 |
43 | // Secure sets the 'Secure' flag on the cookie. Defaults to true (recommended).
44 | // Set this to 'false' in your development environment otherwise the cookie won't
45 | // be sent over an insecure channel. Setting this via the presence of a 'DEV'
46 | // environmental variable is a good way of making sure this won't make it to a
47 | // production environment.
48 | func Secure(s bool) Option {
49 | return func(cs *csrf) {
50 | cs.opts.Secure = s
51 | }
52 | }
53 |
54 | // HttpOnly sets the 'HttpOnly' flag on the cookie. Defaults to true (recommended).
55 | func HttpOnly(h bool) Option {
56 | return func(cs *csrf) {
57 | // Note that the function and field names match the case of the
58 | // related http.Cookie field instead of the "correct" HTTPOnly name
59 | // that golint suggests.
60 | cs.opts.HttpOnly = h
61 | }
62 | }
63 |
64 | // SameSite sets the cookie SameSite attribute. Defaults to blank to maintain
65 | // backwards compatibility, however, Strict is recommended.
66 | //
67 | // SameSite(SameSiteStrictMode) will prevent the cookie from being sent by the
68 | // browser to the target site in all cross-site browsing context, even when
69 | // following a regular link (GET request).
70 | //
71 | // SameSite(SameSiteLaxMode) provides a reasonable balance between security and
72 | // usability for websites that want to maintain user's logged-in session after
73 | // the user arrives from an external link. The session cookie would be allowed
74 | // when following a regular link from an external website while blocking it in
75 | // CSRF-prone request methods (e.g. POST).
76 | //
77 | // This option is only available for go 1.11+.
78 | func SameSite(s SameSiteMode) Option {
79 | return func(cs *csrf) {
80 | cs.opts.SameSite = s
81 | }
82 | }
83 |
84 | // ErrorHandler allows you to change the handler called when CSRF request
85 | // processing encounters an invalid token or request. A typical use would be to
86 | // provide a handler that returns a static HTML file with a HTTP 403 status. By
87 | // default a HTTP 403 status and a plain text CSRF failure reason are served.
88 | //
89 | // Note that a custom error handler can also access the csrf.FailureReason(r)
90 | // function to retrieve the CSRF validation reason from the request context.
91 | func ErrorHandler(h http.Handler) Option {
92 | return func(cs *csrf) {
93 | cs.opts.ErrorHandler = h
94 | }
95 | }
96 |
97 | // RequestHeader allows you to change the request header the CSRF middleware
98 | // inspects. The default is X-CSRF-Token.
99 | func RequestHeader(header string) Option {
100 | return func(cs *csrf) {
101 | cs.opts.RequestHeader = header
102 | }
103 | }
104 |
105 | // FieldName allows you to change the name attribute of the hidden field
106 | // inspected by this package. The default is 'gorilla.csrf.Token'.
107 | func FieldName(name string) Option {
108 | return func(cs *csrf) {
109 | cs.opts.FieldName = name
110 | }
111 | }
112 |
113 | // CookieName changes the name of the CSRF cookie issued to clients.
114 | //
115 | // Note that cookie names should not contain whitespace, commas, semicolons,
116 | // backslashes or control characters as per RFC6265.
117 | func CookieName(name string) Option {
118 | return func(cs *csrf) {
119 | cs.opts.CookieName = name
120 | }
121 | }
122 |
123 | // TrustedOrigins configures a set of origins (Referers) that are considered as trusted.
124 | // This will allow cross-domain CSRF use-cases - e.g. where the front-end is served
125 | // from a different domain than the API server - to correctly pass a CSRF check.
126 | //
127 | // You should only provide origins you own or have full control over.
128 | func TrustedOrigins(origins []string) Option {
129 | return func(cs *csrf) {
130 | cs.opts.TrustedOrigins = origins
131 | }
132 | }
133 |
134 | // setStore sets the store used by the CSRF middleware.
135 | // Note: this is private (for now) to allow for internal API changes.
136 | func setStore(s store) Option {
137 | return func(cs *csrf) {
138 | cs.st = s
139 | }
140 | }
141 |
142 | // parseOptions parses the supplied options functions and returns a configured
143 | // csrf handler.
144 | func parseOptions(h http.Handler, opts ...Option) *csrf {
145 | // Set the handler to call after processing.
146 | cs := &csrf{
147 | h: h,
148 | }
149 |
150 | // Default to true. See Secure & HttpOnly function comments for rationale.
151 | // Set here to allow package users to override the default.
152 | cs.opts.Secure = true
153 | cs.opts.HttpOnly = true
154 |
155 | // Set SameSite=Lax by default, allowing the CSRF cookie to only be sent on
156 | // top-level navigations.
157 | cs.opts.SameSite = SameSiteLaxMode
158 |
159 | // Default; only override this if the package user explicitly calls MaxAge(0)
160 | cs.opts.MaxAge = defaultAge
161 |
162 | // Range over each options function and apply it
163 | // to our csrf type to configure it. Options functions are
164 | // applied in order, with any conflicting options overriding
165 | // earlier calls.
166 | for _, option := range opts {
167 | option(cs)
168 | }
169 |
170 | return cs
171 | }
172 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package csrf (gorilla/csrf) provides Cross Site Request Forgery (CSRF)
3 | prevention middleware for Go web applications & services.
4 |
5 | It includes:
6 |
7 | * The `csrf.Protect` middleware/handler provides CSRF protection on routes
8 | attached to a router or a sub-router.
9 |
10 | * A `csrf.Token` function that provides the token to pass into your response,
11 | whether that be a HTML form or a JSON response body.
12 |
13 | * ... and a `csrf.TemplateField` helper that you can pass into your `html/template`
14 | templates to replace a `{{ .csrfField }}` template tag with a hidden input
15 | field.
16 |
17 | gorilla/csrf is easy to use: add the middleware to individual handlers with
18 | the below:
19 |
20 | CSRF := csrf.Protect([]byte("32-byte-long-auth-key"))
21 | http.HandlerFunc("/route", CSRF(YourHandler))
22 |
23 | ... and then collect the token with `csrf.Token(r)` before passing it to the
24 | template, JSON body or HTTP header (you pick!). gorilla/csrf inspects the form body
25 | (first) and HTTP headers (second) on subsequent POST/PUT/PATCH/DELETE/etc. requests
26 | for the token.
27 |
28 | Note that the authentication key passed to `csrf.Protect([]byte(key))` should be
29 | 32-bytes long and persist across application restarts. Generating a random key
30 | won't allow you to authenticate existing cookies and will break your CSRF
31 | validation.
32 |
33 | Here's the common use-case: HTML forms you want to provide CSRF protection for,
34 | in order to protect malicious POST requests being made:
35 |
36 | package main
37 |
38 | import (
39 | "fmt"
40 | "html/template"
41 | "net/http"
42 |
43 | "github.com/gorilla/csrf"
44 | "github.com/gorilla/mux"
45 | )
46 |
47 | var form = `
48 |
49 |
50 | Sign Up!
51 |
52 |
53 |
64 |
65 |
66 | `
67 |
68 | var t = template.Must(template.New("signup_form.tmpl").Parse(form))
69 |
70 | func main() {
71 | r := mux.NewRouter()
72 | r.HandleFunc("/signup", ShowSignupForm)
73 | // All POST requests without a valid token will return HTTP 403 Forbidden.
74 | // We should also ensure that our mutating (non-idempotent) handler only
75 | // matches on POST requests. We can check that here, at the router level, or
76 | // within the handler itself via r.Method.
77 | r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")
78 |
79 | // Add the middleware to your router by wrapping it.
80 | http.ListenAndServe(":8000",
81 | csrf.Protect([]byte("32-byte-long-auth-key"))(r))
82 | // PS: Don't forget to pass csrf.Secure(false) if you're developing locally
83 | // over plain HTTP (just don't leave it on in production).
84 | }
85 |
86 | func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
87 | // signup_form.tmpl just needs a {{ .csrfField }} template tag for
88 | // csrf.TemplateField to inject the CSRF token into. Easy!
89 | t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
90 | csrf.TemplateTag: csrf.TemplateField(r),
91 | })
92 | }
93 |
94 | func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
95 | // We can trust that requests making it this far have satisfied
96 | // our CSRF protection requirements.
97 | fmt.Fprintf(w, "%v\n", r.PostForm)
98 | }
99 |
100 | Note that the CSRF middleware will (by necessity) consume the request body if the
101 | token is passed via POST form values. If you need to consume this in your
102 | handler, insert your own middleware earlier in the chain to capture the request
103 | body.
104 |
105 | You can also send the CSRF token in the response header. This approach is useful
106 | if you're using a front-end JavaScript framework like Ember or Angular, or are
107 | providing a JSON API:
108 |
109 | package main
110 |
111 | import (
112 | "github.com/gorilla/csrf"
113 | "github.com/gorilla/mux"
114 | )
115 |
116 | func main() {
117 | r := mux.NewRouter()
118 |
119 | api := r.PathPrefix("/api").Subrouter()
120 | api.HandleFunc("/user/:id", GetUser).Methods("GET")
121 |
122 | http.ListenAndServe(":8000",
123 | csrf.Protect([]byte("32-byte-long-auth-key"))(r))
124 | }
125 |
126 | func GetUser(w http.ResponseWriter, r *http.Request) {
127 | // Authenticate the request, get the id from the route params,
128 | // and fetch the user from the DB, etc.
129 |
130 | // Get the token and pass it in the CSRF header. Our JSON-speaking client
131 | // or JavaScript framework can now read the header and return the token in
132 | // in its own "X-CSRF-Token" request header on the subsequent POST.
133 | w.Header().Set("X-CSRF-Token", csrf.Token(r))
134 | b, err := json.Marshal(user)
135 | if err != nil {
136 | http.Error(w, err.Error(), 500)
137 | return
138 | }
139 |
140 | w.Write(b)
141 | }
142 |
143 | If you're writing a client that's supposed to mimic browser behavior, make sure to
144 | send back the CSRF cookie (the default name is _gorilla_csrf, but this can be changed
145 | with the CookieName Option) along with either the X-CSRF-Token header or the gorilla.csrf.Token form field.
146 |
147 | In addition: getting CSRF protection right is important, so here's some background:
148 |
149 | * This library generates unique-per-request (masked) tokens as a mitigation
150 | against the BREACH attack (http://breachattack.com/).
151 |
152 | * The 'base' (unmasked) token is stored in the session, which means that
153 | multiple browser tabs won't cause a user problems as their per-request token
154 | is compared with the base token.
155 |
156 | * Operates on a "whitelist only" approach where safe (non-mutating) HTTP methods
157 | (GET, HEAD, OPTIONS, TRACE) are the *only* methods where token validation is not
158 | enforced.
159 |
160 | * The design is based on the battle-tested Django
161 | (https://docs.djangoproject.com/en/1.8/ref/csrf/) and Ruby on Rails
162 | (http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html)
163 | approaches.
164 |
165 | * Cookies are authenticated and based on the securecookie
166 | (https://github.com/gorilla/securecookie) library. They're also Secure (issued
167 | over HTTPS only) and are HttpOnly by default, because sane defaults are
168 | important.
169 |
170 | * Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens
171 | and the one-time-pad used for masking them.
172 |
173 | This library does not seek to be adventurous.
174 |
175 | */
176 | package csrf
177 |
--------------------------------------------------------------------------------
/helpers.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/subtle"
6 | "encoding/base64"
7 | "fmt"
8 | "html/template"
9 | "net/http"
10 | "net/url"
11 | )
12 |
13 | // Token returns a masked CSRF token ready for passing into HTML template or
14 | // a JSON response body. An empty token will be returned if the middleware
15 | // has not been applied (which will fail subsequent validation).
16 | func Token(r *http.Request) string {
17 | if val, err := contextGet(r, tokenKey); err == nil {
18 | if maskedToken, ok := val.(string); ok {
19 | return maskedToken
20 | }
21 | }
22 |
23 | return ""
24 | }
25 |
26 | // FailureReason makes CSRF validation errors available in the request context.
27 | // This is useful when you want to log the cause of the error or report it to
28 | // client.
29 | func FailureReason(r *http.Request) error {
30 | if val, err := contextGet(r, errorKey); err == nil {
31 | if err, ok := val.(error); ok {
32 | return err
33 | }
34 | }
35 |
36 | return nil
37 | }
38 |
39 | // UnsafeSkipCheck will skip the CSRF check for any requests. This must be
40 | // called before the CSRF middleware.
41 | //
42 | // Note: You should not set this without otherwise securing the request from
43 | // CSRF attacks. The primary use-case for this function is to turn off CSRF
44 | // checks for non-browser clients using authorization tokens against your API.
45 | func UnsafeSkipCheck(r *http.Request) *http.Request {
46 | return contextSave(r, skipCheckKey, true)
47 | }
48 |
49 | // TemplateField is a template helper for html/template that provides an field
50 | // populated with a CSRF token.
51 | //
52 | // Example:
53 | //
54 | // // The following tag in our form.tmpl template:
55 | // {{ .csrfField }}
56 | //
57 | // // ... becomes:
58 | //
59 | func TemplateField(r *http.Request) template.HTML {
60 | if name, err := contextGet(r, formKey); err == nil {
61 | fragment := fmt.Sprintf(``,
62 | name, Token(r))
63 |
64 | return template.HTML(fragment) // #nosec G203
65 | }
66 |
67 | return template.HTML("")
68 | }
69 |
70 | // mask returns a unique-per-request token to mitigate the BREACH attack
71 | // as per http://breachattack.com/#mitigations
72 | //
73 | // The token is generated by XOR'ing a one-time-pad and the base (session) CSRF
74 | // token and returning them together as a 64-byte slice. This effectively
75 | // randomises the token on a per-request basis without breaking multiple browser
76 | // tabs/windows.
77 | func mask(realToken []byte, _ *http.Request) string {
78 | otp, err := generateRandomBytes(tokenLength)
79 | if err != nil {
80 | return ""
81 | }
82 |
83 | // XOR the OTP with the real token to generate a masked token. Append the
84 | // OTP to the front of the masked token to allow unmasking in the subsequent
85 | // request.
86 | return base64.StdEncoding.EncodeToString(append(otp, xorToken(otp, realToken)...))
87 | }
88 |
89 | // unmask splits the issued token (one-time-pad + masked token) and returns the
90 | // unmasked request token for comparison.
91 | func unmask(issued []byte) []byte {
92 | // Issued tokens are always masked and combined with the pad.
93 | if len(issued) != tokenLength*2 {
94 | return nil
95 | }
96 |
97 | // We now know the length of the byte slice.
98 | otp := issued[tokenLength:]
99 | masked := issued[:tokenLength]
100 |
101 | // Unmask the token by XOR'ing it against the OTP used to mask it.
102 | return xorToken(otp, masked)
103 | }
104 |
105 | // requestToken returns the issued token (pad + masked token) from the HTTP POST
106 | // body or HTTP header. It will return nil if the token fails to decode.
107 | func (cs *csrf) requestToken(r *http.Request) ([]byte, error) {
108 | // 1. Check the HTTP header first.
109 | issued := r.Header.Get(cs.opts.RequestHeader)
110 |
111 | // 2. Fall back to the POST (form) value.
112 | if issued == "" {
113 | issued = r.PostFormValue(cs.opts.FieldName)
114 | }
115 |
116 | // 3. Finally, fall back to the multipart form (if set).
117 | if issued == "" && r.MultipartForm != nil {
118 | vals := r.MultipartForm.Value[cs.opts.FieldName]
119 |
120 | if len(vals) > 0 {
121 | issued = vals[0]
122 | }
123 | }
124 |
125 | // Return nil (equivalent to empty byte slice) if no token was found
126 | if issued == "" {
127 | return nil, nil
128 | }
129 |
130 | // Decode the "issued" (pad + masked) token sent in the request. Return a
131 | // nil byte slice on a decoding error (this will fail upstream).
132 | decoded, err := base64.StdEncoding.DecodeString(issued)
133 | if err != nil {
134 | return nil, err
135 | }
136 |
137 | return decoded, nil
138 | }
139 |
140 | // generateRandomBytes returns securely generated random bytes.
141 | // It will return an error if the system's secure random number generator
142 | // fails to function correctly.
143 | func generateRandomBytes(n int) ([]byte, error) {
144 | b := make([]byte, n)
145 | _, err := rand.Read(b)
146 | // err == nil only if len(b) == n
147 | if err != nil {
148 | return nil, err
149 | }
150 |
151 | return b, nil
152 |
153 | }
154 |
155 | // sameOrigin returns true if URLs a and b share the same origin. The same
156 | // origin is defined as host (which includes the port) and scheme.
157 | func sameOrigin(a, b *url.URL) bool {
158 | return (a.Scheme == b.Scheme && a.Host == b.Host)
159 | }
160 |
161 | // compare securely (constant-time) compares the unmasked token from the request
162 | // against the real token from the session.
163 | func compareTokens(a, b []byte) bool {
164 | // This is required as subtle.ConstantTimeCompare does not check for equal
165 | // lengths in Go versions prior to 1.3.
166 | if len(a) != len(b) {
167 | return false
168 | }
169 |
170 | return subtle.ConstantTimeCompare(a, b) == 1
171 | }
172 |
173 | // xorToken XORs tokens ([]byte) to provide unique-per-request CSRF tokens. It
174 | // will return a masked token if the base token is XOR'ed with a one-time-pad.
175 | // An unmasked token will be returned if a masked token is XOR'ed with the
176 | // one-time-pad used to mask it.
177 | func xorToken(a, b []byte) []byte {
178 | n := len(a)
179 | if len(b) < n {
180 | n = len(b)
181 | }
182 |
183 | res := make([]byte, n)
184 |
185 | for i := 0; i < n; i++ {
186 | res[i] = a[i] ^ b[i]
187 | }
188 |
189 | return res
190 | }
191 |
192 | // contains is a helper function to check if a string exists in a slice - e.g.
193 | // whether a HTTP method exists in a list of safe methods.
194 | func contains(vals []string, s string) bool {
195 | for _, v := range vals {
196 | if v == s {
197 | return true
198 | }
199 | }
200 |
201 | return false
202 | }
203 |
204 | // envError stores a CSRF error in the request context.
205 | func envError(r *http.Request, err error) *http.Request {
206 | return contextSave(r, errorKey, err)
207 | }
208 |
--------------------------------------------------------------------------------
/helpers_test.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "encoding/base64"
7 | "fmt"
8 | "io"
9 | "log"
10 | "mime/multipart"
11 | "net/http"
12 | "net/http/httptest"
13 | "net/url"
14 | "strings"
15 | "testing"
16 | "text/template"
17 | )
18 |
19 | var testTemplate = `
20 |
21 |
22 |
25 |
26 |
27 | `
28 |
29 | // Test that our form helpers correctly inject a token into the response body.
30 | func TestFormToken(t *testing.T) {
31 | s := http.NewServeMux()
32 |
33 | // Make the token available outside of the handler for comparison.
34 | var token string
35 | s.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36 | token = Token(r)
37 | t := template.Must((template.New("base").Parse(testTemplate)))
38 | err := t.Execute(w, map[string]interface{}{
39 | TemplateTag: TemplateField(r),
40 | })
41 | if err != nil {
42 | log.Printf("errored during executing the template: %v", err)
43 | }
44 | }))
45 |
46 | r, err := http.NewRequest("GET", "/", nil)
47 | if err != nil {
48 | t.Fatal(err)
49 | }
50 |
51 | rr := httptest.NewRecorder()
52 | p := Protect(testKey)(s)
53 | p.ServeHTTP(rr, r)
54 |
55 | if rr.Code != http.StatusOK {
56 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
57 | rr.Code, http.StatusOK)
58 | }
59 |
60 | if len(token) != base64.StdEncoding.EncodedLen(tokenLength*2) {
61 | t.Fatalf("token length invalid: got %v want %v", len(token), base64.StdEncoding.EncodedLen(tokenLength*2))
62 | }
63 |
64 | if !strings.Contains(rr.Body.String(), token) {
65 | t.Fatalf("token not in response body: got %v want %v", rr.Body.String(), token)
66 | }
67 | }
68 |
69 | // Test that we can extract a CSRF token from a multipart form.
70 | func TestMultipartFormToken(t *testing.T) {
71 | s := http.NewServeMux()
72 |
73 | // Make the token available outside of the handler for comparison.
74 | var token string
75 | s.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
76 | token = Token(r)
77 | t := template.Must((template.New("base").Parse(testTemplate)))
78 | err := t.Execute(w, map[string]interface{}{
79 | TemplateTag: TemplateField(r),
80 | })
81 | if err != nil {
82 | log.Printf("errored during executing the template: %v", err)
83 | }
84 | }))
85 |
86 | r := createRequest("GET", "/", true)
87 |
88 | rr := httptest.NewRecorder()
89 | p := Protect(testKey)(s)
90 | p.ServeHTTP(rr, r)
91 |
92 | // Set up our multipart form
93 | var b bytes.Buffer
94 | mp := multipart.NewWriter(&b)
95 | wr, err := mp.CreateFormField(fieldName)
96 | if err != nil {
97 | t.Fatal(err)
98 | }
99 |
100 | _, err = wr.Write([]byte(token))
101 | if err != nil {
102 | t.Fatal(err)
103 | }
104 |
105 | mp.Close()
106 |
107 | r = httptest.NewRequest("POST", "/", &b)
108 | r.Host = "www.gorillatoolkit.org"
109 |
110 | // Add the multipart header.
111 | r.Header.Set("Content-Type", mp.FormDataContentType())
112 | // Add Origin to pass the same-origin check.
113 | r.Header.Set("Origin", "https://www.gorillatoolkit.org")
114 |
115 | // Send back the issued cookie.
116 | setCookie(rr, r)
117 |
118 | rr = httptest.NewRecorder()
119 | p.ServeHTTP(rr, r)
120 |
121 | if rr.Code != http.StatusOK {
122 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
123 | rr.Code, http.StatusOK)
124 | }
125 |
126 | if body := rr.Body.String(); !strings.Contains(body, token) {
127 | t.Fatalf("token not in response body: got %v want %v", body, token)
128 | }
129 | }
130 |
131 | // TestMaskUnmaskTokens tests that a token traversing the mask -> unmask process
132 | // is correctly unmasked to the original 'real' token.
133 | func TestMaskUnmaskTokens(t *testing.T) {
134 | realToken, err := generateRandomBytes(tokenLength)
135 | if err != nil {
136 | t.Fatal(err)
137 | }
138 |
139 | issued := mask(realToken, nil)
140 | decoded, err := base64.StdEncoding.DecodeString(issued)
141 | if err != nil {
142 | t.Fatal(err)
143 | }
144 |
145 | unmasked := unmask(decoded)
146 | if !compareTokens(unmasked, realToken) {
147 | t.Fatalf("tokens do not match: got %x want %x", unmasked, realToken)
148 | }
149 | }
150 |
151 | // Tests domains that should (or should not) return true for a
152 | // same-origin check.
153 | func TestSameOrigin(t *testing.T) {
154 | var originTests = []struct {
155 | o1 string
156 | o2 string
157 | expected bool
158 | }{
159 | {"https://gorillatoolkit.org/", "https://gorillatoolkit.org", true},
160 | {"http://golang.org/", "http://golang.org/pkg/net/http", true},
161 | {"https://gorillatoolkit.org/", "http://gorillatoolkit.org", false},
162 | {"https://gorillatoolkit.org:3333/", "http://gorillatoolkit.org:4444", false},
163 | }
164 |
165 | for _, origins := range originTests {
166 | a, err := url.Parse(origins.o1)
167 | if err != nil {
168 | t.Fatal(err)
169 | }
170 |
171 | b, err := url.Parse(origins.o2)
172 | if err != nil {
173 | t.Fatal(err)
174 | }
175 |
176 | if sameOrigin(a, b) != origins.expected {
177 | t.Fatalf("origin checking failed: %v and %v, expected %v",
178 | origins.o1, origins.o2, origins.expected)
179 | }
180 | }
181 | }
182 |
183 | func TestXOR(t *testing.T) {
184 | testTokens := []struct {
185 | a []byte
186 | b []byte
187 | expected []byte
188 | }{
189 | {[]byte("goodbye"), []byte("hello"), []byte{15, 10, 3, 8, 13}},
190 | {[]byte("gophers"), []byte("clojure"), []byte{4, 3, 31, 2, 16, 0, 22}},
191 | {nil, []byte("requestToken"), nil},
192 | }
193 |
194 | for _, token := range testTokens {
195 | if res := xorToken(token.a, token.b); res != nil {
196 | if !bytes.Equal(res, token.expected) {
197 | t.Fatalf("xorBytes failed to return the expected result: got %v want %v",
198 | res, token.expected)
199 | }
200 | }
201 | }
202 | }
203 |
204 | // shortReader provides a broken implementation of io.Reader for testing.
205 | type shortReader struct{}
206 |
207 | func (sr shortReader) Read(p []byte) (int, error) {
208 | return len(p) % 2, io.ErrUnexpectedEOF
209 | }
210 |
211 | // TestGenerateRandomBytes tests the (extremely rare) case that crypto/rand does
212 | // not return the expected number of bytes.
213 | func TestGenerateRandomBytes(t *testing.T) {
214 | // Pioneered from https://github.com/justinas/nosurf
215 | original := rand.Reader
216 | rand.Reader = shortReader{}
217 | defer func() {
218 | rand.Reader = original
219 | }()
220 |
221 | b, err := generateRandomBytes(tokenLength)
222 | if err == nil {
223 | t.Fatalf("generateRandomBytes did not report a short read: only read %d bytes", len(b))
224 | }
225 | }
226 |
227 | func TestTemplateField(t *testing.T) {
228 | s := http.NewServeMux()
229 |
230 | // Make the token & template field available outside of the handler.
231 | var token string
232 | var templateField string
233 | s.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
234 | token = Token(r)
235 | templateField = string(TemplateField(r))
236 | t := template.Must((template.New("base").Parse(testTemplate)))
237 | err := t.Execute(w, map[string]interface{}{
238 | TemplateTag: TemplateField(r),
239 | })
240 | if err != nil {
241 | log.Printf("errored during executing the template: %v", err)
242 | }
243 | }))
244 |
245 | testFieldName := "custom_field_name"
246 | r := createRequest("GET", "/", false)
247 | // r, err := http.NewRequest("GET", "/", nil)
248 |
249 | rr := httptest.NewRecorder()
250 | p := Protect(testKey, FieldName(testFieldName))(s)
251 | p.ServeHTTP(rr, r)
252 |
253 | expectedField := fmt.Sprintf(``,
254 | testFieldName, token)
255 |
256 | if rr.Code != http.StatusOK {
257 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
258 | rr.Code, http.StatusOK)
259 | }
260 |
261 | if templateField != expectedField {
262 | t.Fatalf("custom FieldName was not set correctly: got %v want %v",
263 | templateField, expectedField)
264 | }
265 | }
266 |
267 | func TestCompareTokens(t *testing.T) {
268 | // Go's subtle.ConstantTimeCompare prior to 1.3 did not check for matching
269 | // lengths.
270 | a := []byte("")
271 | b := []byte("an-actual-token")
272 |
273 | if v := compareTokens(a, b); v == true {
274 | t.Fatalf("compareTokens failed on different tokens: got %v want %v", v, !v)
275 | }
276 | }
277 |
278 | func TestUnsafeSkipCSRFCheck(t *testing.T) {
279 | s := http.NewServeMux()
280 | skipCheck := func(h http.Handler) http.Handler {
281 | fn := func(w http.ResponseWriter, r *http.Request) {
282 | r = UnsafeSkipCheck(r)
283 | h.ServeHTTP(w, r)
284 | }
285 |
286 | return http.HandlerFunc(fn)
287 | }
288 |
289 | var teapot = 418
290 |
291 | // Issue a POST request without a CSRF token in the request.
292 | s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
293 | // Set a non-200 header to make the test explicit.
294 | w.WriteHeader(teapot)
295 | }))
296 |
297 | r := createRequest("POST", "/", false)
298 |
299 | // Must be used prior to the CSRF handler being invoked.
300 | p := skipCheck(Protect(testKey)(s))
301 | rr := httptest.NewRecorder()
302 | p.ServeHTTP(rr, r)
303 |
304 | if status := rr.Code; status != teapot {
305 | t.Fatalf("middleware failed to skip this request: got %v want %v",
306 | status, teapot)
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/csrf.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net/http"
8 | "net/url"
9 | "slices"
10 |
11 | "github.com/gorilla/securecookie"
12 | )
13 |
14 | // CSRF token length in bytes.
15 | const tokenLength = 32
16 |
17 | // Context/session keys & prefixes
18 | const (
19 | tokenKey string = "gorilla.csrf.Token" // #nosec G101
20 | formKey string = "gorilla.csrf.Form" // #nosec G101
21 | errorKey string = "gorilla.csrf.Error"
22 | skipCheckKey string = "gorilla.csrf.Skip"
23 | cookieName string = "_gorilla_csrf"
24 | errorPrefix string = "gorilla/csrf: "
25 | )
26 |
27 | type contextKey string
28 |
29 | // PlaintextHTTPContextKey is the context key used to store whether the request
30 | // is being served via plaintext HTTP. This is used to signal to the middleware
31 | // that strict Referer checking should not be enforced as is done for HTTPS by
32 | // default.
33 | const PlaintextHTTPContextKey contextKey = "plaintext"
34 |
35 | var (
36 | // The name value used in form fields.
37 | fieldName = tokenKey
38 | // defaultAge sets the default MaxAge for cookies.
39 | defaultAge = 3600 * 12
40 | // The default HTTP request header to inspect
41 | headerName = "X-CSRF-Token"
42 | // Idempotent (safe) methods as defined by RFC7231 section 4.2.2.
43 | safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
44 | )
45 |
46 | // TemplateTag provides a default template tag - e.g. {{ .csrfField }} - for use
47 | // with the TemplateField function.
48 | var TemplateTag = "csrfField"
49 |
50 | var (
51 | // ErrNoReferer is returned when a HTTPS request provides an empty Referer
52 | // header.
53 | ErrNoReferer = errors.New("referer not supplied")
54 | // ErrBadOrigin is returned when the Origin header is present and is not a
55 | // trusted origin.
56 | ErrBadOrigin = errors.New("origin invalid")
57 | // ErrBadReferer is returned when the scheme & host in the URL do not match
58 | // the supplied Referer header.
59 | ErrBadReferer = errors.New("referer invalid")
60 | // ErrNoToken is returned if no CSRF token is supplied in the request.
61 | ErrNoToken = errors.New("CSRF token not found in request")
62 | // ErrBadToken is returned if the CSRF token in the request does not match
63 | // the token in the session, or is otherwise malformed.
64 | ErrBadToken = errors.New("CSRF token invalid")
65 | )
66 |
67 | // SameSiteMode allows a server to define a cookie attribute making it impossible for
68 | // the browser to send this cookie along with cross-site requests. The main
69 | // goal is to mitigate the risk of cross-origin information leakage, and provide
70 | // some protection against cross-site request forgery attacks.
71 | //
72 | // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
73 | type SameSiteMode int
74 |
75 | // SameSite options
76 | const (
77 | // SameSiteDefaultMode sets the `SameSite` cookie attribute, which is
78 | // invalid in some older browsers due to changes in the SameSite spec. These
79 | // browsers will not send the cookie to the server.
80 | // csrf uses SameSiteLaxMode (SameSite=Lax) as the default as of v1.7.0+
81 | SameSiteDefaultMode SameSiteMode = iota + 1
82 | SameSiteLaxMode
83 | SameSiteStrictMode
84 | SameSiteNoneMode
85 | )
86 |
87 | type csrf struct {
88 | h http.Handler
89 | sc *securecookie.SecureCookie
90 | st store
91 | opts options
92 | }
93 |
94 | // options contains the optional settings for the CSRF middleware.
95 | type options struct {
96 | MaxAge int
97 | Domain string
98 | Path string
99 | // Note that the function and field names match the case of the associated
100 | // http.Cookie field instead of the "correct" HTTPOnly name that golint suggests.
101 | HttpOnly bool
102 | Secure bool
103 | SameSite SameSiteMode
104 | RequestHeader string
105 | FieldName string
106 | ErrorHandler http.Handler
107 | CookieName string
108 | TrustedOrigins []string
109 | }
110 |
111 | // Protect is HTTP middleware that provides Cross-Site Request Forgery
112 | // protection.
113 | //
114 | // It securely generates a masked (unique-per-request) token that
115 | // can be embedded in the HTTP response (e.g. form field or HTTP header).
116 | // The original (unmasked) token is stored in the session, which is inaccessible
117 | // by an attacker (provided you are using HTTPS). Subsequent requests are
118 | // expected to include this token, which is compared against the session token.
119 | // Requests that do not provide a matching token are served with a HTTP 403
120 | // 'Forbidden' error response.
121 | //
122 | // Example:
123 | //
124 | // package main
125 | //
126 | // import (
127 | // "html/template"
128 | //
129 | // "github.com/gorilla/csrf"
130 | // "github.com/gorilla/mux"
131 | // )
132 | //
133 | // var t = template.Must(template.New("signup_form.tmpl").Parse(form))
134 | //
135 | // func main() {
136 | // r := mux.NewRouter()
137 | //
138 | // r.HandleFunc("/signup", GetSignupForm)
139 | // // POST requests without a valid token will return a HTTP 403 Forbidden.
140 | // r.HandleFunc("/signup/post", PostSignupForm)
141 | //
142 | // // Add the middleware to your router.
143 | // http.ListenAndServe(":8000",
144 | // // Note that the authentication key provided should be 32 bytes
145 | // // long and persist across application restarts.
146 | // csrf.Protect([]byte("32-byte-long-auth-key"))(r))
147 | // }
148 | //
149 | // func GetSignupForm(w http.ResponseWriter, r *http.Request) {
150 | // // signup_form.tmpl just needs a {{ .csrfField }} template tag for
151 | // // csrf.TemplateField to inject the CSRF token into. Easy!
152 | // t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
153 | // csrf.TemplateTag: csrf.TemplateField(r),
154 | // })
155 | // // We could also retrieve the token directly from csrf.Token(r) and
156 | // // set it in the request header - w.Header.Set("X-CSRF-Token", token)
157 | // // This is useful if you're sending JSON to clients or a front-end JavaScript
158 | // // framework.
159 | // }
160 | func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler {
161 | return func(h http.Handler) http.Handler {
162 | cs := parseOptions(h, opts...)
163 |
164 | // Set the defaults if no options have been specified
165 | if cs.opts.ErrorHandler == nil {
166 | cs.opts.ErrorHandler = http.HandlerFunc(unauthorizedHandler)
167 | }
168 |
169 | if cs.opts.MaxAge < 0 {
170 | // Default of 12 hours
171 | cs.opts.MaxAge = defaultAge
172 | }
173 |
174 | if cs.opts.FieldName == "" {
175 | cs.opts.FieldName = fieldName
176 | }
177 |
178 | if cs.opts.CookieName == "" {
179 | cs.opts.CookieName = cookieName
180 | }
181 |
182 | if cs.opts.RequestHeader == "" {
183 | cs.opts.RequestHeader = headerName
184 | }
185 |
186 | // Create an authenticated securecookie instance.
187 | if cs.sc == nil {
188 | cs.sc = securecookie.New(authKey, nil)
189 | // Use JSON serialization (faster than one-off gob encoding)
190 | cs.sc.SetSerializer(securecookie.JSONEncoder{})
191 | // Set the MaxAge of the underlying securecookie.
192 | cs.sc.MaxAge(cs.opts.MaxAge)
193 | }
194 |
195 | if cs.st == nil {
196 | // Default to the cookieStore
197 | cs.st = &cookieStore{
198 | name: cs.opts.CookieName,
199 | maxAge: cs.opts.MaxAge,
200 | secure: cs.opts.Secure,
201 | httpOnly: cs.opts.HttpOnly,
202 | sameSite: cs.opts.SameSite,
203 | path: cs.opts.Path,
204 | domain: cs.opts.Domain,
205 | sc: cs.sc,
206 | }
207 | }
208 |
209 | return cs
210 | }
211 | }
212 |
213 | // Implements http.Handler for the csrf type.
214 | func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
215 | // Skip the check if directed to. This should always be a bool.
216 | if val, err := contextGet(r, skipCheckKey); err == nil {
217 | if skip, ok := val.(bool); ok {
218 | if skip {
219 | cs.h.ServeHTTP(w, r)
220 | return
221 | }
222 | }
223 | }
224 |
225 | // Retrieve the token from the session.
226 | // An error represents either a cookie that failed HMAC validation
227 | // or that doesn't exist.
228 | realToken, err := cs.st.Get(r)
229 | if err != nil || len(realToken) != tokenLength {
230 | // If there was an error retrieving the token, the token doesn't exist
231 | // yet, or it's the wrong length, generate a new token.
232 | // Note that the new token will (correctly) fail validation downstream
233 | // as it will no longer match the request token.
234 | realToken, err = generateRandomBytes(tokenLength)
235 | if err != nil {
236 | r = envError(r, err)
237 | cs.opts.ErrorHandler.ServeHTTP(w, r)
238 | return
239 | }
240 |
241 | // Save the new (real) token in the session store.
242 | err = cs.st.Save(realToken, w)
243 | if err != nil {
244 | r = envError(r, err)
245 | cs.opts.ErrorHandler.ServeHTTP(w, r)
246 | return
247 | }
248 | }
249 |
250 | // Save the masked token to the request context
251 | r = contextSave(r, tokenKey, mask(realToken, r))
252 | // Save the field name to the request context
253 | r = contextSave(r, formKey, cs.opts.FieldName)
254 |
255 | // HTTP methods not defined as idempotent ("safe") under RFC7231 require
256 | // inspection.
257 | if !contains(safeMethods, r.Method) {
258 | var isPlaintext bool
259 | val := r.Context().Value(PlaintextHTTPContextKey)
260 | if val != nil {
261 | isPlaintext, _ = val.(bool)
262 | }
263 |
264 | // take a copy of the request URL to avoid mutating the original
265 | // attached to the request.
266 | // set the scheme & host based on the request context as these are not
267 | // populated by default for server requests
268 | // ref: https://pkg.go.dev/net/http#Request
269 | requestURL := *r.URL // shallow clone
270 |
271 | requestURL.Scheme = "https"
272 | if isPlaintext {
273 | requestURL.Scheme = "http"
274 | }
275 | if requestURL.Host == "" {
276 | requestURL.Host = r.Host
277 | }
278 |
279 | // if we have an Origin header, check it against our allowlist
280 | origin := r.Header.Get("Origin")
281 | if origin != "" {
282 | parsedOrigin, err := url.Parse(origin)
283 | if err != nil {
284 | r = envError(r, ErrBadOrigin)
285 | cs.opts.ErrorHandler.ServeHTTP(w, r)
286 | return
287 | }
288 | if !sameOrigin(&requestURL, parsedOrigin) && !slices.Contains(cs.opts.TrustedOrigins, parsedOrigin.Host) {
289 | r = envError(r, ErrBadOrigin)
290 | cs.opts.ErrorHandler.ServeHTTP(w, r)
291 | return
292 | }
293 | }
294 |
295 | // If we are serving via TLS and have no Origin header, prevent against
296 | // CSRF via HTTP machine in the middle attacks by enforcing strict
297 | // Referer origin checks. Consider an attacker who performs a
298 | // successful HTTP Machine-in-the-Middle attack and uses this to inject
299 | // a form and cause submission to our origin. We strictly disallow
300 | // cleartext HTTP origins and evaluate the domain against an allowlist.
301 | if origin == "" && !isPlaintext {
302 | // Fetch the Referer value. Call the error handler if it's empty or
303 | // otherwise fails to parse.
304 | referer, err := url.Parse(r.Referer())
305 | if err != nil || referer.String() == "" {
306 | r = envError(r, ErrNoReferer)
307 | cs.opts.ErrorHandler.ServeHTTP(w, r)
308 | return
309 | }
310 |
311 | // disallow cleartext HTTP referers when serving via TLS
312 | if referer.Scheme == "http" {
313 | r = envError(r, ErrBadReferer)
314 | cs.opts.ErrorHandler.ServeHTTP(w, r)
315 | return
316 | }
317 |
318 | // If the request is being served via TLS and the Referer is not the
319 | // same origin, check the domain against our allowlist. We only
320 | // check when we have host information from the referer.
321 | if referer.Host != "" && referer.Host != r.Host && !slices.Contains(cs.opts.TrustedOrigins, referer.Host) {
322 | r = envError(r, ErrBadReferer)
323 | cs.opts.ErrorHandler.ServeHTTP(w, r)
324 | return
325 | }
326 | }
327 |
328 | // Retrieve the combined token (pad + masked) token...
329 | maskedToken, err := cs.requestToken(r)
330 | if err != nil {
331 | r = envError(r, ErrBadToken)
332 | cs.opts.ErrorHandler.ServeHTTP(w, r)
333 | return
334 | }
335 |
336 | if maskedToken == nil {
337 | r = envError(r, ErrNoToken)
338 | cs.opts.ErrorHandler.ServeHTTP(w, r)
339 | return
340 | }
341 |
342 | // ... and unmask it.
343 | requestToken := unmask(maskedToken)
344 |
345 | // Compare the request token against the real token
346 | if !compareTokens(requestToken, realToken) {
347 | r = envError(r, ErrBadToken)
348 | cs.opts.ErrorHandler.ServeHTTP(w, r)
349 | return
350 | }
351 |
352 | }
353 |
354 | // Set the Vary: Cookie header to protect clients from caching the response.
355 | w.Header().Add("Vary", "Cookie")
356 |
357 | // Call the wrapped handler/router on success.
358 | cs.h.ServeHTTP(w, r)
359 | // Clear the request context after the handler has completed.
360 | contextClear(r)
361 | }
362 |
363 | // PlaintextHTTPRequest accepts as input a http.Request and returns a new
364 | // http.Request with the PlaintextHTTPContextKey set to true. This is used to
365 | // signal to the CSRF middleware that the request is being served over plaintext
366 | // HTTP and that Referer-based origin allow-listing checks should be skipped.
367 | func PlaintextHTTPRequest(r *http.Request) *http.Request {
368 | ctx := context.WithValue(r.Context(), PlaintextHTTPContextKey, true)
369 | return r.WithContext(ctx)
370 | }
371 |
372 | // unauthorizedhandler sets a HTTP 403 Forbidden status and writes the
373 | // CSRF failure reason to the response.
374 | func unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
375 | http.Error(w, fmt.Sprintf("%s - %s",
376 | http.StatusText(http.StatusForbidden), FailureReason(r)),
377 | http.StatusForbidden)
378 | }
379 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gorilla/csrf
2 |
3 | 
4 | [](https://codecov.io/github/gorilla/csrf)
5 | [](https://godoc.org/github.com/gorilla/csrf)
6 | [](https://sourcegraph.com/github.com/gorilla/csrf?badge)
7 |
8 |
9 | 
10 |
11 | gorilla/csrf is a HTTP middleware library that provides [cross-site request
12 | forgery](http://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/) (CSRF)
13 | protection. It includes:
14 |
15 | - The `csrf.Protect` middleware/handler provides CSRF protection on routes
16 | attached to a router or a sub-router.
17 | - A `csrf.Token` function that provides the token to pass into your response,
18 | whether that be a HTML form or a JSON response body.
19 | - ... and a `csrf.TemplateField` helper that you can pass into your `html/template`
20 | templates to replace a `{{ .csrfField }}` template tag with a hidden input
21 | field.
22 |
23 | gorilla/csrf is designed to work with any Go web framework, including:
24 |
25 | - The [Gorilla](https://www.gorillatoolkit.org/) toolkit
26 | - Go's built-in [net/http](http://golang.org/pkg/net/http/) package
27 | - [Goji](https://goji.io) - see the [tailored fork](https://github.com/goji/csrf)
28 | - [Gin](https://github.com/gin-gonic/gin)
29 | - [Echo](https://github.com/labstack/echo)
30 | - ... and any other router/framework that rallies around Go's `http.Handler` interface.
31 |
32 | gorilla/csrf is also compatible with middleware 'helper' libraries like
33 | [Alice](https://github.com/justinas/alice) and [Negroni](https://github.com/codegangsta/negroni).
34 |
35 | ## Contents
36 |
37 | * [Install](#install)
38 | * [Examples](#examples)
39 | + [HTML Forms](#html-forms)
40 | + [JavaScript Applications](#javascript-applications)
41 | + [Google App Engine](#google-app-engine)
42 | + [Setting SameSite](#setting-samesite)
43 | + [Setting Options](#setting-options)
44 | * [Design Notes](#design-notes)
45 | * [License](#license)
46 |
47 | ## Install
48 |
49 | With a properly configured Go toolchain:
50 |
51 | ```sh
52 | go get github.com/gorilla/csrf
53 | ```
54 |
55 | ## Examples
56 |
57 | - [HTML Forms](#html-forms)
58 | - [JavaScript Apps](#javascript-applications)
59 | - [Google App Engine](#google-app-engine)
60 | - [Setting SameSite](#setting-samesite)
61 | - [Setting Options](#setting-options)
62 |
63 | gorilla/csrf is easy to use: add the middleware to your router with
64 | the below:
65 |
66 | ```go
67 | CSRF := csrf.Protect([]byte("32-byte-long-auth-key"))
68 | http.ListenAndServe(":8000", CSRF(r))
69 | ```
70 |
71 | ...and then collect the token with `csrf.Token(r)` in your handlers before
72 | passing it to the template, JSON body or HTTP header (see below).
73 |
74 | Note that the authentication key passed to `csrf.Protect([]byte(key))` should:
75 | - be 32-bytes long
76 | - persist across application restarts.
77 | - kept secret from potential malicious users - do not hardcode it into the source code, especially not in open-source applications.
78 |
79 | Generating a random key won't allow you to authenticate existing cookies and will break your CSRF
80 | validation.
81 |
82 | gorilla/csrf inspects the HTTP headers (first) and form body (second) on
83 | subsequent POST/PUT/PATCH/DELETE/etc. requests for the token.
84 |
85 | ### HTML Forms
86 |
87 | Here's the common use-case: HTML forms you want to provide CSRF protection for,
88 | in order to protect malicious POST requests being made:
89 |
90 | ```go
91 | package main
92 |
93 | import (
94 | "net/http"
95 |
96 | "github.com/gorilla/csrf"
97 | "github.com/gorilla/mux"
98 | )
99 |
100 | func main() {
101 | r := mux.NewRouter()
102 | r.HandleFunc("/signup", ShowSignupForm)
103 | // All POST requests without a valid token will return HTTP 403 Forbidden.
104 | // We should also ensure that our mutating (non-idempotent) handler only
105 | // matches on POST requests. We can check that here, at the router level, or
106 | // within the handler itself via r.Method.
107 | r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")
108 |
109 | // Add the middleware to your router by wrapping it.
110 | http.ListenAndServe(":8000",
111 | csrf.Protect([]byte("32-byte-long-auth-key"))(r))
112 | // PS: Don't forget to pass csrf.Secure(false) if you're developing locally
113 | // over plain HTTP (just don't leave it on in production).
114 | }
115 |
116 | func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
117 | // signup_form.tmpl just needs a {{ .csrfField }} template tag for
118 | // csrf.TemplateField to inject the CSRF token into. Easy!
119 | t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
120 | csrf.TemplateTag: csrf.TemplateField(r),
121 | })
122 | // We could also retrieve the token directly from csrf.Token(r) and
123 | // set it in the request header - w.Header.Set("X-CSRF-Token", token)
124 | // This is useful if you're sending JSON to clients or a front-end JavaScript
125 | // framework.
126 | }
127 |
128 | func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
129 | // We can trust that requests making it this far have satisfied
130 | // our CSRF protection requirements.
131 | }
132 | ```
133 |
134 | Note that the CSRF middleware will (by necessity) consume the request body if the
135 | token is passed via POST form values. If you need to consume this in your
136 | handler, insert your own middleware earlier in the chain to capture the request
137 | body.
138 |
139 | ### JavaScript Applications
140 |
141 | This approach is useful if you're using a front-end JavaScript framework like
142 | React, Ember or Angular, and are providing a JSON API. Specifically, we need
143 | to provide a way for our front-end fetch/AJAX calls to pass the token on each
144 | fetch (AJAX/XMLHttpRequest) request. We achieve this by:
145 |
146 | - Parsing the token from the `` field generated by the
147 | `csrf.TemplateField(r)` helper, or passing it back in a response header.
148 | - Sending this token back on every request
149 | - Ensuring our cookie is attached to the request so that the form/header
150 | value can be compared to the cookie value.
151 |
152 | We'll also look at applying selective CSRF protection using
153 | [gorilla/mux's](https://www.gorillatoolkit.org/pkg/mux) sub-routers,
154 | as we don't handle any POST/PUT/DELETE requests with our top-level router.
155 |
156 | ```go
157 | package main
158 |
159 | import (
160 | "github.com/gorilla/csrf"
161 | "github.com/gorilla/mux"
162 | )
163 |
164 | func main() {
165 | r := mux.NewRouter()
166 | csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))
167 |
168 | api := r.PathPrefix("/api").Subrouter()
169 | api.Use(csrfMiddleware)
170 | api.HandleFunc("/user/{id}", GetUser).Methods("GET")
171 |
172 | http.ListenAndServe(":8000", r)
173 | }
174 |
175 | func GetUser(w http.ResponseWriter, r *http.Request) {
176 | // Authenticate the request, get the id from the route params,
177 | // and fetch the user from the DB, etc.
178 |
179 | // Get the token and pass it in the CSRF header. Our JSON-speaking client
180 | // or JavaScript framework can now read the header and return the token in
181 | // in its own "X-CSRF-Token" request header on the subsequent POST.
182 | w.Header().Set("X-CSRF-Token", csrf.Token(r))
183 | b, err := json.Marshal(user)
184 | if err != nil {
185 | http.Error(w, err.Error(), 500)
186 | return
187 | }
188 |
189 | w.Write(b)
190 | }
191 | ```
192 |
193 | In our JavaScript application, we should read the token from the response
194 | headers and pass it in a request header for all requests. Here's what that
195 | looks like when using [Axios](https://github.com/axios/axios), a popular
196 | JavaScript HTTP client library:
197 |
198 | ```js
199 | // You can alternatively parse the response header for the X-CSRF-Token, and
200 | // store that instead, if you followed the steps above to write the token to a
201 | // response header.
202 | let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value
203 |
204 | // via https://github.com/axios/axios#creating-an-instance
205 | const instance = axios.create({
206 | baseURL: "https://example.com/api/",
207 | timeout: 1000,
208 | headers: { "X-CSRF-Token": csrfToken }
209 | })
210 |
211 | // Now, any HTTP request you make will include the csrfToken from the page,
212 | // provided you update the csrfToken variable for each render.
213 | try {
214 | let resp = await instance.post(endpoint, formData)
215 | // Do something with resp
216 | } catch (err) {
217 | // Handle the exception
218 | }
219 | ```
220 |
221 | If you plan to host your JavaScript application on another domain, you can use the Trusted Origins
222 | feature to allow the host of your JavaScript application to make requests to your Go application. Observe the example below:
223 |
224 |
225 | ```go
226 | package main
227 |
228 | import (
229 | "github.com/gorilla/csrf"
230 | "github.com/gorilla/mux"
231 | )
232 |
233 | func main() {
234 | r := mux.NewRouter()
235 | csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"), csrf.TrustedOrigins([]string{"ui.domain.com"}))
236 |
237 | api := r.PathPrefix("/api").Subrouter()
238 | api.Use(csrfMiddleware)
239 | api.HandleFunc("/user/{id}", GetUser).Methods("GET")
240 |
241 | http.ListenAndServe(":8000", r)
242 | }
243 |
244 | func GetUser(w http.ResponseWriter, r *http.Request) {
245 | // Authenticate the request, get the id from the route params,
246 | // and fetch the user from the DB, etc.
247 |
248 | // Get the token and pass it in the CSRF header. Our JSON-speaking client
249 | // or JavaScript framework can now read the header and return the token in
250 | // in its own "X-CSRF-Token" request header on the subsequent POST.
251 | w.Header().Set("X-CSRF-Token", csrf.Token(r))
252 | b, err := json.Marshal(user)
253 | if err != nil {
254 | http.Error(w, err.Error(), 500)
255 | return
256 | }
257 |
258 | w.Write(b)
259 | }
260 | ```
261 |
262 | 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.
263 |
264 | ### Google App Engine
265 |
266 | If you're using [Google App
267 | Engine](https://cloud.google.com/appengine/docs/go/how-requests-are-handled#Go_Requests_and_HTTP),
268 | (first-generation) which doesn't allow you to hook into the default `http.ServeMux` directly,
269 | you can still use gorilla/csrf (and gorilla/mux):
270 |
271 | ```go
272 | package app
273 |
274 | // Remember: appengine has its own package main
275 | func init() {
276 | r := mux.NewRouter()
277 | r.HandleFunc("/", IndexHandler)
278 | // ...
279 |
280 | // We pass our CSRF-protected router to the DefaultServeMux
281 | http.Handle("/", csrf.Protect([]byte(your-key))(r))
282 | }
283 | ```
284 |
285 | Note: You can ignore this if you're using the
286 | [second-generation](https://cloud.google.com/appengine/docs/go/) Go runtime
287 | on App Engine (Go 1.11 and above).
288 |
289 | ### Setting SameSite
290 |
291 | Go 1.11 introduced the option to set the SameSite attribute in cookies. This is
292 | valuable if a developer wants to instruct a browser to not include cookies during
293 | a cross site request. SameSiteStrictMode prevents all cross site requests from including
294 | the cookie. SameSiteLaxMode prevents CSRF prone requests (POST) from including the cookie
295 | but allows the cookie to be included in GET requests to support external linking.
296 |
297 | ```go
298 | func main() {
299 | CSRF := csrf.Protect(
300 | []byte("a-32-byte-long-key-goes-here"),
301 | // instruct the browser to never send cookies during cross site requests
302 | csrf.SameSite(csrf.SameSiteStrictMode),
303 | )
304 |
305 | r := mux.NewRouter()
306 | r.HandleFunc("/signup", GetSignupForm)
307 | r.HandleFunc("/signup/post", PostSignupForm)
308 |
309 | http.ListenAndServe(":8000", CSRF(r))
310 | }
311 | ```
312 |
313 | ### Cookie path
314 |
315 | By default, CSRF cookies are set on the path of the request.
316 |
317 | This can create issues, if the request is done from one path to a different path.
318 |
319 | You might want to set up a root path for all the cookies; that way, the CSRF will always work across all your paths.
320 |
321 | ```
322 | CSRF := csrf.Protect(
323 | []byte("a-32-byte-long-key-goes-here"),
324 | csrf.Path("/"),
325 | )
326 | ```
327 |
328 | ### Setting Options
329 |
330 | What about providing your own error handler and changing the HTTP header the
331 | package inspects on requests? (i.e. an existing API you're porting to Go). Well,
332 | gorilla/csrf provides options for changing these as you see fit:
333 |
334 | ```go
335 | func main() {
336 | CSRF := csrf.Protect(
337 | []byte("a-32-byte-long-key-goes-here"),
338 | csrf.RequestHeader("Authenticity-Token"),
339 | csrf.FieldName("authenticity_token"),
340 | csrf.ErrorHandler(http.HandlerFunc(serverError(403))),
341 | )
342 |
343 | r := mux.NewRouter()
344 | r.HandleFunc("/signup", GetSignupForm)
345 | r.HandleFunc("/signup/post", PostSignupForm)
346 |
347 | http.ListenAndServe(":8000", CSRF(r))
348 | }
349 | ```
350 |
351 | Not too bad, right?
352 |
353 | If there's something you're confused about or a feature you would like to see
354 | added, open an issue.
355 |
356 | ## Design Notes
357 |
358 | Getting CSRF protection right is important, so here's some background:
359 |
360 | - This library generates unique-per-request (masked) tokens as a mitigation
361 | against the [BREACH attack](http://breachattack.com/).
362 | - The 'base' (unmasked) token is stored in the session, which means that
363 | multiple browser tabs won't cause a user problems as their per-request token
364 | is compared with the base token.
365 | - Operates on a "whitelist only" approach where safe (non-mutating) HTTP methods
366 | (GET, HEAD, OPTIONS, TRACE) are the _only_ methods where token validation is not
367 | enforced.
368 | - The design is based on the battle-tested
369 | [Django](https://docs.djangoproject.com/en/1.8/ref/csrf/) and [Ruby on
370 | Rails](http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html)
371 | approaches.
372 | - Cookies are authenticated and based on the [securecookie](https://github.com/gorilla/securecookie)
373 | library. They're also Secure (issued over HTTPS only) and are HttpOnly
374 | by default, because sane defaults are important.
375 | - Cookie SameSite attribute (prevents cookies from being sent by a browser
376 | during cross site requests) are not set by default to maintain backwards compatibility
377 | for legacy systems. The SameSite attribute can be set with the SameSite option.
378 | - Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens
379 | and the one-time-pad used for masking them.
380 |
381 | This library does not seek to be adventurous.
382 |
383 | ## License
384 |
385 | BSD licensed. See the LICENSE file for details.
386 |
--------------------------------------------------------------------------------
/csrf_test.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | var testKey = []byte("keep-it-secret-keep-it-safe-----")
12 | var testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
13 |
14 | // TestProtect is a high-level test to make sure the middleware returns the
15 | // wrapped handler with a 200 OK status.
16 | func TestProtect(t *testing.T) {
17 | s := http.NewServeMux()
18 | s.HandleFunc("/", testHandler)
19 |
20 | r := createRequest("GET", "/", false)
21 |
22 | rr := httptest.NewRecorder()
23 | p := Protect(testKey)(s)
24 | p.ServeHTTP(rr, r)
25 |
26 | if rr.Code != http.StatusOK {
27 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
28 | rr.Code, http.StatusOK)
29 | }
30 |
31 | if rr.Header().Get("Set-Cookie") == "" {
32 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
33 | }
34 |
35 | cookie := rr.Header().Get("Set-Cookie")
36 | if !strings.Contains(cookie, "HttpOnly") || !strings.Contains(cookie,
37 | "Secure") {
38 | t.Fatalf("cookie does not default to Secure & HttpOnly: got %v", cookie)
39 | }
40 | }
41 |
42 | // TestCookieOptions is a test to make sure the middleware correctly sets cookie options
43 | func TestCookieOptions(t *testing.T) {
44 | s := http.NewServeMux()
45 | s.HandleFunc("/", testHandler)
46 |
47 | r := createRequest("GET", "/", false)
48 |
49 | rr := httptest.NewRecorder()
50 | p := Protect(testKey, CookieName("nameoverride"), Secure(false), HttpOnly(false), Path("/pathoverride"), Domain("domainoverride"), MaxAge(173))(s)
51 | p.ServeHTTP(rr, r)
52 |
53 | if rr.Header().Get("Set-Cookie") == "" {
54 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
55 | }
56 |
57 | cookie := rr.Header().Get("Set-Cookie")
58 | if strings.Contains(cookie, "HttpOnly") {
59 | t.Fatalf("cookie does not respect HttpOnly option: got %v do not want HttpOnly", cookie)
60 | }
61 | if strings.Contains(cookie, "Secure") {
62 | t.Fatalf("cookie does not respect Secure option: got %v do not want Secure", cookie)
63 | }
64 | if !strings.Contains(cookie, "nameoverride=") {
65 | t.Fatalf("cookie does not respect CookieName option: got %v want %v", cookie, "nameoverride=")
66 | }
67 | if !strings.Contains(cookie, "Domain=domainoverride") {
68 | t.Fatalf("cookie does not respect Domain option: got %v want %v", cookie, "Domain=domainoverride")
69 | }
70 | if !strings.Contains(cookie, "Max-Age=173") {
71 | t.Fatalf("cookie does not respect MaxAge option: got %v want %v", cookie, "Max-Age=173")
72 | }
73 | }
74 |
75 | // Test that idempotent methods return a 200 OK status and that non-idempotent
76 | // methods return a 403 Forbidden status when a CSRF cookie is not present.
77 | func TestMethods(t *testing.T) {
78 | s := http.NewServeMux()
79 | s.HandleFunc("/", testHandler)
80 | p := Protect(testKey)(s)
81 |
82 | // Test idempontent ("safe") methods
83 | for _, method := range safeMethods {
84 | r := createRequest(method, "/", false)
85 |
86 | rr := httptest.NewRecorder()
87 | p.ServeHTTP(rr, r)
88 |
89 | if rr.Code != http.StatusOK {
90 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
91 | rr.Code, http.StatusOK)
92 | }
93 |
94 | if rr.Header().Get("Set-Cookie") == "" {
95 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
96 | }
97 | }
98 |
99 | // Test non-idempotent methods (should return a 403 without a cookie set)
100 | nonIdempotent := []string{"POST", "PUT", "DELETE", "PATCH"}
101 | for _, method := range nonIdempotent {
102 | r := createRequest(method, "/", false)
103 |
104 | rr := httptest.NewRecorder()
105 | p.ServeHTTP(rr, r)
106 |
107 | if rr.Code != http.StatusForbidden {
108 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
109 | rr.Code, http.StatusOK)
110 | }
111 |
112 | if rr.Header().Get("Set-Cookie") == "" {
113 | t.Fatalf("cookie not set: got %q", rr.Header().Get("Set-Cookie"))
114 | }
115 | }
116 | }
117 |
118 | // Tests for failure if the cookie containing the session does not exist on a
119 | // POST request.
120 | func TestNoCookie(t *testing.T) {
121 | s := http.NewServeMux()
122 | p := Protect(testKey)(s)
123 |
124 | // POST the token back in the header.
125 | r := createRequest("POST", "/", false)
126 |
127 | rr := httptest.NewRecorder()
128 | p.ServeHTTP(rr, r)
129 |
130 | if rr.Code != http.StatusForbidden {
131 | t.Fatalf("middleware failed to reject a non-existent cookie: got %v want %v",
132 | rr.Code, http.StatusForbidden)
133 | }
134 | }
135 |
136 | // TestBadCookie tests for failure when a cookie header is modified (malformed).
137 | func TestBadCookie(t *testing.T) {
138 | s := http.NewServeMux()
139 | p := Protect(testKey)(s)
140 |
141 | var token string
142 | s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
143 | token = Token(r)
144 | }))
145 |
146 | // Obtain a CSRF cookie via a GET request.
147 | r := createRequest("GET", "/", false)
148 |
149 | rr := httptest.NewRecorder()
150 | p.ServeHTTP(rr, r)
151 |
152 | // POST the token back in the header.
153 | r = createRequest("POST", "/", false)
154 |
155 | // Replace the cookie prefix
156 | badHeader := strings.Replace(cookieName+"=", rr.Header().Get("Set-Cookie"), "_badCookie", -1)
157 | r.Header.Set("Cookie", badHeader)
158 | r.Header.Set("X-CSRF-Token", token)
159 | r.Header.Set("Referer", "http://www.gorillatoolkit.org/")
160 |
161 | rr = httptest.NewRecorder()
162 | p.ServeHTTP(rr, r)
163 |
164 | if rr.Code != http.StatusForbidden {
165 | t.Fatalf("middleware failed to reject a bad cookie: got %v want %v",
166 | rr.Code, http.StatusForbidden)
167 | }
168 | }
169 |
170 | // Responses should set a "Vary: Cookie" header to protect client/proxy caching.
171 | func TestVaryHeader(t *testing.T) {
172 | s := http.NewServeMux()
173 | s.HandleFunc("/", testHandler)
174 | p := Protect(testKey)(s)
175 |
176 | r := createRequest("GET", "/", true)
177 |
178 | rr := httptest.NewRecorder()
179 | p.ServeHTTP(rr, r)
180 |
181 | if rr.Code != http.StatusOK {
182 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
183 | rr.Code, http.StatusOK)
184 | }
185 |
186 | if rr.Header().Get("Vary") != "Cookie" {
187 | t.Fatalf("vary header not set: got %q want %q", rr.Header().Get("Vary"), "Cookie")
188 | }
189 | }
190 |
191 | // TestNoReferer checks that HTTPS requests with no Referer header fail.
192 | func TestNoReferer(t *testing.T) {
193 | s := http.NewServeMux()
194 | s.HandleFunc("/", testHandler)
195 | p := Protect(testKey)(s)
196 |
197 | r := createRequest("POST", "https://golang.org/", true)
198 |
199 | rr := httptest.NewRecorder()
200 | p.ServeHTTP(rr, r)
201 |
202 | if rr.Code != http.StatusForbidden {
203 | t.Fatalf("middleware failed reject an empty Referer header: got %v want %v",
204 | rr.Code, http.StatusForbidden)
205 | }
206 | }
207 |
208 | // TestBadReferer checks that HTTPS requests with a Referer that does not
209 | // match the request URL correctly fail CSRF validation.
210 | func TestBadReferer(t *testing.T) {
211 | s := http.NewServeMux()
212 | p := Protect(testKey)(s)
213 |
214 | var token string
215 | s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
216 | token = Token(r)
217 | }))
218 |
219 | // Obtain a CSRF cookie via a GET request.
220 | r := createRequest("GET", "/", true)
221 | rr := httptest.NewRecorder()
222 | p.ServeHTTP(rr, r)
223 |
224 | // POST the token back in the header.
225 | r = createRequest("POST", "/", true)
226 | setCookie(rr, r)
227 | r.Header.Set("X-CSRF-Token", token)
228 |
229 | // Set a non-matching Referer header.
230 | r.Header.Set("Referer", "http://golang.org/")
231 |
232 | rr = httptest.NewRecorder()
233 | p.ServeHTTP(rr, r)
234 |
235 | if rr.Code != http.StatusForbidden {
236 | t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v",
237 | rr.Code, http.StatusForbidden)
238 | }
239 | }
240 |
241 | // TestTrustedReferer checks that HTTPS requests with a Referer that does not
242 | // match the request URL correctly but is a trusted origin pass CSRF validation.
243 | func TestTrustedReferer(t *testing.T) {
244 |
245 | testTable := []struct {
246 | trustedOrigin []string
247 | shouldPass bool
248 | }{
249 | {[]string{"golang.org"}, true},
250 | {[]string{"api.example.com", "golang.org"}, true},
251 | {[]string{"http://golang.org"}, false},
252 | {[]string{"https://golang.org"}, false},
253 | {[]string{"http://example.com"}, false},
254 | {[]string{"example.com"}, false},
255 | }
256 |
257 | for _, item := range testTable {
258 | t.Run(fmt.Sprintf("TrustedOrigin: %v", item.trustedOrigin), func(t *testing.T) {
259 |
260 | s := http.NewServeMux()
261 |
262 | p := Protect(testKey, TrustedOrigins(item.trustedOrigin))(s)
263 |
264 | var token string
265 | s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
266 | token = Token(r)
267 | }))
268 |
269 | // Obtain a CSRF cookie via a GET request.
270 | r := createRequest("GET", "/", true)
271 |
272 | rr := httptest.NewRecorder()
273 | p.ServeHTTP(rr, r)
274 |
275 | // POST the token back in the header.
276 | r = createRequest("POST", "/", true)
277 |
278 | setCookie(rr, r)
279 | r.Header.Set("X-CSRF-Token", token)
280 |
281 | // Set a non-matching Referer header.
282 | r.Header.Set("Referer", "https://golang.org/")
283 |
284 | rr = httptest.NewRecorder()
285 | p.ServeHTTP(rr, r)
286 |
287 | if item.shouldPass {
288 | if rr.Code != http.StatusOK {
289 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
290 | rr.Code, http.StatusOK)
291 | }
292 | } else {
293 | if rr.Code != http.StatusForbidden {
294 | t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v",
295 | rr.Code, http.StatusForbidden)
296 | }
297 | }
298 | })
299 | }
300 | }
301 |
302 | // Requests with a valid Referer should pass.
303 | func TestWithReferer(t *testing.T) {
304 | s := http.NewServeMux()
305 | p := Protect(testKey)(s)
306 |
307 | var token string
308 | s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
309 | token = Token(r)
310 | }))
311 |
312 | // Obtain a CSRF cookie via a GET request.
313 | r := createRequest("GET", "/", true)
314 | rr := httptest.NewRecorder()
315 | p.ServeHTTP(rr, r)
316 |
317 | // POST the token back in the header.
318 | r = createRequest("POST", "/", true)
319 |
320 | setCookie(rr, r)
321 | r.Header.Set("X-CSRF-Token", token)
322 | r.Header.Set("Referer", "https://www.gorillatoolkit.org/")
323 |
324 | rr = httptest.NewRecorder()
325 | p.ServeHTTP(rr, r)
326 |
327 | if rr.Code != http.StatusOK {
328 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
329 | rr.Code, http.StatusOK)
330 | }
331 | }
332 |
333 | // Requests without a token should fail with ErrNoToken.
334 | func TestNoTokenProvided(t *testing.T) {
335 | var finalErr error
336 |
337 | s := http.NewServeMux()
338 | p := Protect(testKey, ErrorHandler(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
339 | finalErr = FailureReason(r)
340 | })))(s)
341 |
342 | var token string
343 | s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
344 | token = Token(r)
345 | }))
346 | // Obtain a CSRF cookie via a GET request.
347 | r := createRequest("GET", "/", true)
348 |
349 | rr := httptest.NewRecorder()
350 | p.ServeHTTP(rr, r)
351 |
352 | // POST the token back in the header.
353 | r = createRequest("POST", "/", true)
354 |
355 | setCookie(rr, r)
356 | // By accident we use the wrong header name for the token...
357 | r.Header.Set("X-CSRF-nekot", token)
358 | r.Header.Set("Referer", "https://www.gorillatoolkit.org/")
359 |
360 | rr = httptest.NewRecorder()
361 | p.ServeHTTP(rr, r)
362 |
363 | if finalErr != nil && finalErr != ErrNoToken {
364 | t.Fatalf("middleware failed to return correct error: got '%v' want '%v'", finalErr, ErrNoToken)
365 | }
366 | }
367 |
368 | func setCookie(rr *httptest.ResponseRecorder, r *http.Request) {
369 | r.Header.Set("Cookie", rr.Header().Get("Set-Cookie"))
370 | }
371 |
372 | func TestProtectScenarios(t *testing.T) {
373 | tests := []struct {
374 | name string
375 | safeMethod bool
376 | originUntrusted bool
377 | originHTTP bool
378 | originTrusted bool
379 | secureRequest bool
380 | refererTrusted bool
381 | refererUntrusted bool
382 | refererHTTPDowngrade bool
383 | refererRelative bool
384 | tokenValid bool
385 | tokenInvalid bool
386 | want bool
387 | }{
388 | {
389 | name: "safe method pass",
390 | safeMethod: true,
391 | want: true,
392 | },
393 | {
394 | name: "cleartext POST with trusted origin & valid token pass",
395 | originHTTP: true,
396 | tokenValid: true,
397 | want: true,
398 | },
399 | {
400 | name: "cleartext POST with untrusted origin reject",
401 | originUntrusted: true,
402 | tokenValid: true,
403 | },
404 | {
405 | name: "cleartext POST with HTTP origin & invalid token reject",
406 | originHTTP: true,
407 | },
408 | {
409 | name: "cleartext POST without origin with valid token pass",
410 | tokenValid: true,
411 | want: true,
412 | },
413 | {
414 | name: "cleartext POST without origin with invalid token reject",
415 | },
416 | {
417 | name: "TLS POST with HTTP origin & no referer & valid token reject",
418 | tokenValid: true,
419 | secureRequest: true,
420 | originHTTP: true,
421 | },
422 | {
423 | name: "TLS POST without origin and without referer reject",
424 | secureRequest: true,
425 | tokenValid: true,
426 | },
427 | {
428 | name: "TLS POST without origin with untrusted referer reject",
429 | secureRequest: true,
430 | refererUntrusted: true,
431 | tokenValid: true,
432 | },
433 | {
434 | name: "TLS POST without origin with trusted referer & valid token pass",
435 | secureRequest: true,
436 | refererTrusted: true,
437 | tokenValid: true,
438 | want: true,
439 | },
440 | {
441 | name: "TLS POST without origin from _cleartext_ same domain referer with valid token reject",
442 | secureRequest: true,
443 | refererHTTPDowngrade: true,
444 | tokenValid: true,
445 | },
446 | {
447 | name: "TLS POST without origin from relative referer with valid token pass",
448 | secureRequest: true,
449 | refererRelative: true,
450 | tokenValid: true,
451 | want: true,
452 | },
453 | {
454 | name: "TLS POST without origin from relative referer with invalid token reject",
455 | secureRequest: true,
456 | refererRelative: true,
457 | tokenInvalid: true,
458 | },
459 | }
460 |
461 | for _, tt := range tests {
462 | t.Run(tt.name, func(t *testing.T) {
463 | var token string
464 | var flag bool
465 | mux := http.NewServeMux()
466 | mux.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
467 | token = Token(r)
468 | }))
469 | mux.Handle("/submit", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
470 | flag = true
471 | }))
472 | p := Protect(testKey)(mux)
473 |
474 | // Obtain a CSRF cookie via a GET request.
475 | r := createRequest("GET", "/", tt.secureRequest)
476 | rr := httptest.NewRecorder()
477 | p.ServeHTTP(rr, r)
478 |
479 | r = createRequest("POST", "/submit", tt.secureRequest)
480 | if tt.safeMethod {
481 | r = createRequest("GET", "/submit", tt.secureRequest)
482 | }
483 |
484 | // Set the Origin header
485 | switch {
486 | case tt.originUntrusted:
487 | r.Header.Set("Origin", "http://www.untrusted-origin.org")
488 | case tt.originTrusted:
489 | r.Header.Set("Origin", "https://www.gorillatoolkit.org")
490 | case tt.originHTTP:
491 | r.Header.Set("Origin", "http://www.gorillatoolkit.org")
492 | }
493 |
494 | // Set the Referer header
495 | switch {
496 | case tt.refererTrusted:
497 | p = Protect(testKey, TrustedOrigins([]string{"external-trusted-origin.test"}))(mux)
498 | r.Header.Set("Referer", "https://external-trusted-origin.test/foobar")
499 | case tt.refererUntrusted:
500 | r.Header.Set("Referer", "http://www.invalid-referer.org")
501 | case tt.refererHTTPDowngrade:
502 | r.Header.Set("Referer", "http://www.gorillatoolkit.org/foobar")
503 | case tt.refererRelative:
504 | r.Header.Set("Referer", "/foobar")
505 | }
506 |
507 | // Set the CSRF token & associated cookie
508 | switch {
509 | case tt.tokenInvalid:
510 | setCookie(rr, r)
511 | r.Header.Set("X-CSRF-Token", "this-is-an-invalid-token")
512 | case tt.tokenValid:
513 | setCookie(rr, r)
514 | r.Header.Set("X-CSRF-Token", token)
515 | }
516 |
517 | rr = httptest.NewRecorder()
518 | p.ServeHTTP(rr, r)
519 |
520 | if tt.want && rr.Code != http.StatusOK {
521 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
522 | rr.Code, http.StatusOK)
523 | }
524 |
525 | if tt.want && !flag {
526 | t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
527 | flag, true)
528 |
529 | }
530 | if !tt.want && flag {
531 | t.Fatalf("middleware failed to reject the request: got %v want %v", flag, false)
532 | }
533 | })
534 | }
535 | }
536 |
537 | func createRequest(method, path string, useTLS bool) *http.Request {
538 | r := httptest.NewRequest(method, path, nil)
539 | r.Host = "www.gorillatoolkit.org"
540 | if !useTLS {
541 | return PlaintextHTTPRequest(r)
542 | }
543 | return r
544 | }
545 |
--------------------------------------------------------------------------------
/vendor/github.com/gorilla/securecookie/securecookie.go:
--------------------------------------------------------------------------------
1 | // Copyright 2012 The Gorilla Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package securecookie
6 |
7 | import (
8 | "bytes"
9 | "crypto/aes"
10 | "crypto/cipher"
11 | "crypto/hmac"
12 | "crypto/rand"
13 | "crypto/sha256"
14 | "crypto/subtle"
15 | "encoding/base64"
16 | "encoding/gob"
17 | "encoding/json"
18 | "fmt"
19 | "hash"
20 | "io"
21 | "strconv"
22 | "strings"
23 | "time"
24 | )
25 |
26 | // Error is the interface of all errors returned by functions in this library.
27 | type Error interface {
28 | error
29 |
30 | // IsUsage returns true for errors indicating the client code probably
31 | // uses this library incorrectly. For example, the client may have
32 | // failed to provide a valid hash key, or may have failed to configure
33 | // the Serializer adequately for encoding value.
34 | IsUsage() bool
35 |
36 | // IsDecode returns true for errors indicating that a cookie could not
37 | // be decoded and validated. Since cookies are usually untrusted
38 | // user-provided input, errors of this type should be expected.
39 | // Usually, the proper action is simply to reject the request.
40 | IsDecode() bool
41 |
42 | // IsInternal returns true for unexpected errors occurring in the
43 | // securecookie implementation.
44 | IsInternal() bool
45 |
46 | // Cause, if it returns a non-nil value, indicates that this error was
47 | // propagated from some underlying library. If this method returns nil,
48 | // this error was raised directly by this library.
49 | //
50 | // Cause is provided principally for debugging/logging purposes; it is
51 | // rare that application logic should perform meaningfully different
52 | // logic based on Cause. See, for example, the caveats described on
53 | // (MultiError).Cause().
54 | Cause() error
55 | }
56 |
57 | // errorType is a bitmask giving the error type(s) of an cookieError value.
58 | type errorType int
59 |
60 | const (
61 | usageError = errorType(1 << iota)
62 | decodeError
63 | internalError
64 | )
65 |
66 | type cookieError struct {
67 | typ errorType
68 | msg string
69 | cause error
70 | }
71 |
72 | func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 }
73 | func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 }
74 | func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 }
75 |
76 | func (e cookieError) Cause() error { return e.cause }
77 |
78 | func (e cookieError) Error() string {
79 | parts := []string{"securecookie: "}
80 | if e.msg == "" {
81 | parts = append(parts, "error")
82 | } else {
83 | parts = append(parts, e.msg)
84 | }
85 | if c := e.Cause(); c != nil {
86 | parts = append(parts, " - caused by: ", c.Error())
87 | }
88 | return strings.Join(parts, "")
89 | }
90 |
91 | var (
92 | errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"}
93 |
94 | errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"}
95 | errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"}
96 | errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"}
97 | errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"}
98 |
99 | errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"}
100 | errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"}
101 | errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"}
102 | errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"}
103 | errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"}
104 | errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."}
105 | errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."}
106 |
107 | // ErrMacInvalid indicates that cookie decoding failed because the HMAC
108 | // could not be extracted and verified. Direct use of this error
109 | // variable is deprecated; it is public only for legacy compatibility,
110 | // and may be privatized in the future, as it is rarely useful to
111 | // distinguish between this error and other Error implementations.
112 | ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"}
113 | )
114 |
115 | // Codec defines an interface to encode and decode cookie values.
116 | type Codec interface {
117 | Encode(name string, value interface{}) (string, error)
118 | Decode(name, value string, dst interface{}) error
119 | }
120 |
121 | // New returns a new SecureCookie.
122 | //
123 | // hashKey is required, used to authenticate values using HMAC. Create it using
124 | // GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes.
125 | //
126 | // blockKey is optional, used to encrypt values. Create it using
127 | // GenerateRandomKey(). The key length must correspond to the key size
128 | // of the encryption algorithm. For AES, used by default, valid lengths are
129 | // 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
130 | // The default encoder used for cookie serialization is encoding/gob.
131 | //
132 | // Note that keys created using GenerateRandomKey() are not automatically
133 | // persisted. New keys will be created when the application is restarted, and
134 | // previously issued cookies will not be able to be decoded.
135 | func New(hashKey, blockKey []byte) *SecureCookie {
136 | s := &SecureCookie{
137 | hashKey: hashKey,
138 | blockKey: blockKey,
139 | hashFunc: sha256.New,
140 | maxAge: 86400 * 30,
141 | maxLength: 4096,
142 | sz: GobEncoder{},
143 | }
144 | if len(hashKey) == 0 {
145 | s.err = errHashKeyNotSet
146 | }
147 | if blockKey != nil {
148 | s.BlockFunc(aes.NewCipher)
149 | }
150 | return s
151 | }
152 |
153 | // SecureCookie encodes and decodes authenticated and optionally encrypted
154 | // cookie values.
155 | type SecureCookie struct {
156 | hashKey []byte
157 | hashFunc func() hash.Hash
158 | blockKey []byte
159 | block cipher.Block
160 | maxLength int
161 | maxAge int64
162 | minAge int64
163 | err error
164 | sz Serializer
165 | // For testing purposes, the function that returns the current timestamp.
166 | // If not set, it will use time.Now().UTC().Unix().
167 | timeFunc func() int64
168 | }
169 |
170 | // Serializer provides an interface for providing custom serializers for cookie
171 | // values.
172 | type Serializer interface {
173 | Serialize(src interface{}) ([]byte, error)
174 | Deserialize(src []byte, dst interface{}) error
175 | }
176 |
177 | // GobEncoder encodes cookie values using encoding/gob. This is the simplest
178 | // encoder and can handle complex types via gob.Register.
179 | type GobEncoder struct{}
180 |
181 | // JSONEncoder encodes cookie values using encoding/json. Users who wish to
182 | // encode complex types need to satisfy the json.Marshaller and
183 | // json.Unmarshaller interfaces.
184 | type JSONEncoder struct{}
185 |
186 | // NopEncoder does not encode cookie values, and instead simply accepts a []byte
187 | // (as an interface{}) and returns a []byte. This is particularly useful when
188 | // you encoding an object upstream and do not wish to re-encode it.
189 | type NopEncoder struct{}
190 |
191 | // MaxLength restricts the maximum length, in bytes, for the cookie value.
192 | //
193 | // Default is 4096, which is the maximum value accepted by Internet Explorer.
194 | func (s *SecureCookie) MaxLength(value int) *SecureCookie {
195 | s.maxLength = value
196 | return s
197 | }
198 |
199 | // MaxAge restricts the maximum age, in seconds, for the cookie value.
200 | //
201 | // Default is 86400 * 30. Set it to 0 for no restriction.
202 | func (s *SecureCookie) MaxAge(value int) *SecureCookie {
203 | s.maxAge = int64(value)
204 | return s
205 | }
206 |
207 | // MinAge restricts the minimum age, in seconds, for the cookie value.
208 | //
209 | // Default is 0 (no restriction).
210 | func (s *SecureCookie) MinAge(value int) *SecureCookie {
211 | s.minAge = int64(value)
212 | return s
213 | }
214 |
215 | // HashFunc sets the hash function used to create HMAC.
216 | //
217 | // Default is crypto/sha256.New.
218 | func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
219 | s.hashFunc = f
220 | return s
221 | }
222 |
223 | // BlockFunc sets the encryption function used to create a cipher.Block.
224 | //
225 | // Default is crypto/aes.New.
226 | func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie {
227 | if s.blockKey == nil {
228 | s.err = errBlockKeyNotSet
229 | } else if block, err := f(s.blockKey); err == nil {
230 | s.block = block
231 | } else {
232 | s.err = cookieError{cause: err, typ: usageError}
233 | }
234 | return s
235 | }
236 |
237 | // Encoding sets the encoding/serialization method for cookies.
238 | //
239 | // Default is encoding/gob. To encode special structures using encoding/gob,
240 | // they must be registered first using gob.Register().
241 | func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie {
242 | s.sz = sz
243 |
244 | return s
245 | }
246 |
247 | // Encode encodes a cookie value.
248 | //
249 | // It serializes, optionally encrypts, signs with a message authentication code,
250 | // and finally encodes the value.
251 | //
252 | // The name argument is the cookie name. It is stored with the encoded value.
253 | // The value argument is the value to be encoded. It can be any value that can
254 | // be encoded using the currently selected serializer; see SetSerializer().
255 | //
256 | // It is the client's responsibility to ensure that value, when encoded using
257 | // the current serialization/encryption settings on s and then base64-encoded,
258 | // is shorter than the maximum permissible length.
259 | func (s *SecureCookie) Encode(name string, value interface{}) (string, error) {
260 | if s.err != nil {
261 | return "", s.err
262 | }
263 | if s.hashKey == nil {
264 | s.err = errHashKeyNotSet
265 | return "", s.err
266 | }
267 | var err error
268 | var b []byte
269 | // 1. Serialize.
270 | if b, err = s.sz.Serialize(value); err != nil {
271 | return "", cookieError{cause: err, typ: usageError}
272 | }
273 | // 2. Encrypt (optional).
274 | if s.block != nil {
275 | if b, err = encrypt(s.block, b); err != nil {
276 | return "", cookieError{cause: err, typ: usageError}
277 | }
278 | }
279 | b = encode(b)
280 | // 3. Create MAC for "name|date|value". Extra pipe to be used later.
281 | b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b))
282 | mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1])
283 | // Append mac, remove name.
284 | b = append(b, mac...)[len(name)+1:]
285 | // 4. Encode to base64.
286 | b = encode(b)
287 | // 5. Check length.
288 | if s.maxLength != 0 && len(b) > s.maxLength {
289 | return "", fmt.Errorf("%s: %d", errEncodedValueTooLong, len(b))
290 | }
291 | // Done.
292 | return string(b), nil
293 | }
294 |
295 | // Decode decodes a cookie value.
296 | //
297 | // It decodes, verifies a message authentication code, optionally decrypts and
298 | // finally deserializes the value.
299 | //
300 | // The name argument is the cookie name. It must be the same name used when
301 | // it was stored. The value argument is the encoded cookie value. The dst
302 | // argument is where the cookie will be decoded. It must be a pointer.
303 | func (s *SecureCookie) Decode(name, value string, dst interface{}) error {
304 | if s.err != nil {
305 | return s.err
306 | }
307 | if s.hashKey == nil {
308 | s.err = errHashKeyNotSet
309 | return s.err
310 | }
311 | // 1. Check length.
312 | if s.maxLength != 0 && len(value) > s.maxLength {
313 | return fmt.Errorf("%s: %d", errValueToDecodeTooLong, len(value))
314 | }
315 | // 2. Decode from base64.
316 | b, err := decode([]byte(value))
317 | if err != nil {
318 | return err
319 | }
320 | // 3. Verify MAC. Value is "date|value|mac".
321 | parts := bytes.SplitN(b, []byte("|"), 3)
322 | if len(parts) != 3 {
323 | return ErrMacInvalid
324 | }
325 | h := hmac.New(s.hashFunc, s.hashKey)
326 | b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...)
327 | if err = verifyMac(h, b, parts[2]); err != nil {
328 | return err
329 | }
330 | // 4. Verify date ranges.
331 | var t1 int64
332 | if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil {
333 | return errTimestampInvalid
334 | }
335 | t2 := s.timestamp()
336 | if s.minAge != 0 && t1 > t2-s.minAge {
337 | return errTimestampTooNew
338 | }
339 | if s.maxAge != 0 && t1 < t2-s.maxAge {
340 | return errTimestampExpired
341 | }
342 | // 5. Decrypt (optional).
343 | b, err = decode(parts[1])
344 | if err != nil {
345 | return err
346 | }
347 | if s.block != nil {
348 | if b, err = decrypt(s.block, b); err != nil {
349 | return err
350 | }
351 | }
352 | // 6. Deserialize.
353 | if err = s.sz.Deserialize(b, dst); err != nil {
354 | return cookieError{cause: err, typ: decodeError}
355 | }
356 | // Done.
357 | return nil
358 | }
359 |
360 | // timestamp returns the current timestamp, in seconds.
361 | //
362 | // For testing purposes, the function that generates the timestamp can be
363 | // overridden. If not set, it will return time.Now().UTC().Unix().
364 | func (s *SecureCookie) timestamp() int64 {
365 | if s.timeFunc == nil {
366 | return time.Now().UTC().Unix()
367 | }
368 | return s.timeFunc()
369 | }
370 |
371 | // Authentication -------------------------------------------------------------
372 |
373 | // createMac creates a message authentication code (MAC).
374 | func createMac(h hash.Hash, value []byte) []byte {
375 | h.Write(value)
376 | return h.Sum(nil)
377 | }
378 |
379 | // verifyMac verifies that a message authentication code (MAC) is valid.
380 | func verifyMac(h hash.Hash, value []byte, mac []byte) error {
381 | mac2 := createMac(h, value)
382 | // Check that both MACs are of equal length, as subtle.ConstantTimeCompare
383 | // does not do this prior to Go 1.4.
384 | if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 {
385 | return nil
386 | }
387 | return ErrMacInvalid
388 | }
389 |
390 | // Encryption -----------------------------------------------------------------
391 |
392 | // encrypt encrypts a value using the given block in counter mode.
393 | //
394 | // A random initialization vector ( https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) ) with the length of the
395 | // block size is prepended to the resulting ciphertext.
396 | func encrypt(block cipher.Block, value []byte) ([]byte, error) {
397 | iv := GenerateRandomKey(block.BlockSize())
398 | if iv == nil {
399 | return nil, errGeneratingIV
400 | }
401 | // Encrypt it.
402 | stream := cipher.NewCTR(block, iv)
403 | stream.XORKeyStream(value, value)
404 | // Return iv + ciphertext.
405 | return append(iv, value...), nil
406 | }
407 |
408 | // decrypt decrypts a value using the given block in counter mode.
409 | //
410 | // The value to be decrypted must be prepended by a initialization vector
411 | // ( https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) ) with the length of the block size.
412 | func decrypt(block cipher.Block, value []byte) ([]byte, error) {
413 | size := block.BlockSize()
414 | if len(value) > size {
415 | // Extract iv.
416 | iv := value[:size]
417 | // Extract ciphertext.
418 | value = value[size:]
419 | // Decrypt it.
420 | stream := cipher.NewCTR(block, iv)
421 | stream.XORKeyStream(value, value)
422 | return value, nil
423 | }
424 | return nil, errDecryptionFailed
425 | }
426 |
427 | // Serialization --------------------------------------------------------------
428 |
429 | // Serialize encodes a value using gob.
430 | func (e GobEncoder) Serialize(src interface{}) ([]byte, error) {
431 | buf := new(bytes.Buffer)
432 | enc := gob.NewEncoder(buf)
433 | if err := enc.Encode(src); err != nil {
434 | return nil, cookieError{cause: err, typ: usageError}
435 | }
436 | return buf.Bytes(), nil
437 | }
438 |
439 | // Deserialize decodes a value using gob.
440 | func (e GobEncoder) Deserialize(src []byte, dst interface{}) error {
441 | dec := gob.NewDecoder(bytes.NewBuffer(src))
442 | if err := dec.Decode(dst); err != nil {
443 | return cookieError{cause: err, typ: decodeError}
444 | }
445 | return nil
446 | }
447 |
448 | // Serialize encodes a value using encoding/json.
449 | func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) {
450 | buf := new(bytes.Buffer)
451 | enc := json.NewEncoder(buf)
452 | if err := enc.Encode(src); err != nil {
453 | return nil, cookieError{cause: err, typ: usageError}
454 | }
455 | return buf.Bytes(), nil
456 | }
457 |
458 | // Deserialize decodes a value using encoding/json.
459 | func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error {
460 | dec := json.NewDecoder(bytes.NewReader(src))
461 | if err := dec.Decode(dst); err != nil {
462 | return cookieError{cause: err, typ: decodeError}
463 | }
464 | return nil
465 | }
466 |
467 | // Serialize passes a []byte through as-is.
468 | func (e NopEncoder) Serialize(src interface{}) ([]byte, error) {
469 | if b, ok := src.([]byte); ok {
470 | return b, nil
471 | }
472 |
473 | return nil, errValueNotByte
474 | }
475 |
476 | // Deserialize passes a []byte through as-is.
477 | func (e NopEncoder) Deserialize(src []byte, dst interface{}) error {
478 | if dat, ok := dst.(*[]byte); ok {
479 | *dat = src
480 | return nil
481 | }
482 | return errValueNotBytePtr
483 | }
484 |
485 | // Encoding -------------------------------------------------------------------
486 |
487 | // encode encodes a value using base64.
488 | func encode(value []byte) []byte {
489 | encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value)))
490 | base64.URLEncoding.Encode(encoded, value)
491 | return encoded
492 | }
493 |
494 | // decode decodes a cookie using base64.
495 | func decode(value []byte) ([]byte, error) {
496 | decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value)))
497 | b, err := base64.URLEncoding.Decode(decoded, value)
498 | if err != nil {
499 | return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"}
500 | }
501 | return decoded[:b], nil
502 | }
503 |
504 | // Helpers --------------------------------------------------------------------
505 |
506 | // GenerateRandomKey creates a random key with the given length in bytes.
507 | // On failure, returns nil.
508 | //
509 | // Note that keys created using `GenerateRandomKey()` are not automatically
510 | // persisted. New keys will be created when the application is restarted, and
511 | // previously issued cookies will not be able to be decoded.
512 | //
513 | // Callers should explicitly check for the possibility of a nil return, treat
514 | // it as a failure of the system random number generator, and not continue.
515 | func GenerateRandomKey(length int) []byte {
516 | k := make([]byte, length)
517 | if _, err := io.ReadFull(rand.Reader, k); err != nil {
518 | return nil
519 | }
520 | return k
521 | }
522 |
523 | // CodecsFromPairs returns a slice of SecureCookie instances.
524 | //
525 | // It is a convenience function to create a list of codecs for key rotation. Note
526 | // that the generated Codecs will have the default options applied: callers
527 | // should iterate over each Codec and type-assert the underlying *SecureCookie to
528 | // change these.
529 | //
530 | // Example:
531 | //
532 | // codecs := securecookie.CodecsFromPairs(
533 | // []byte("new-hash-key"),
534 | // []byte("new-block-key"),
535 | // []byte("old-hash-key"),
536 | // []byte("old-block-key"),
537 | // )
538 | //
539 | // // Modify each instance.
540 | // for _, s := range codecs {
541 | // if cookie, ok := s.(*securecookie.SecureCookie); ok {
542 | // cookie.MaxAge(86400 * 7)
543 | // cookie.SetSerializer(securecookie.JSONEncoder{})
544 | // cookie.HashFunc(sha512.New512_256)
545 | // }
546 | // }
547 | func CodecsFromPairs(keyPairs ...[]byte) []Codec {
548 | codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2)
549 | for i := 0; i < len(keyPairs); i += 2 {
550 | var blockKey []byte
551 | if i+1 < len(keyPairs) {
552 | blockKey = keyPairs[i+1]
553 | }
554 | codecs[i/2] = New(keyPairs[i], blockKey)
555 | }
556 | return codecs
557 | }
558 |
559 | // EncodeMulti encodes a cookie value using a group of codecs.
560 | //
561 | // The codecs are tried in order. Multiple codecs are accepted to allow
562 | // key rotation.
563 | //
564 | // On error, may return a MultiError.
565 | func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) {
566 | if len(codecs) == 0 {
567 | return "", errNoCodecs
568 | }
569 |
570 | var errors MultiError
571 | for _, codec := range codecs {
572 | encoded, err := codec.Encode(name, value)
573 | if err == nil {
574 | return encoded, nil
575 | }
576 | errors = append(errors, err)
577 | }
578 | return "", errors
579 | }
580 |
581 | // DecodeMulti decodes a cookie value using a group of codecs.
582 | //
583 | // The codecs are tried in order. Multiple codecs are accepted to allow
584 | // key rotation.
585 | //
586 | // On error, may return a MultiError.
587 | func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error {
588 | if len(codecs) == 0 {
589 | return errNoCodecs
590 | }
591 |
592 | var errors MultiError
593 | for _, codec := range codecs {
594 | err := codec.Decode(name, value, dst)
595 | if err == nil {
596 | return nil
597 | }
598 | errors = append(errors, err)
599 | }
600 | return errors
601 | }
602 |
603 | // MultiError groups multiple errors.
604 | type MultiError []error
605 |
606 | func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) }
607 | func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) }
608 | func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) }
609 |
610 | // Cause returns nil for MultiError; there is no unique underlying cause in the
611 | // general case.
612 | //
613 | // Note: we could conceivably return a non-nil Cause only when there is exactly
614 | // one child error with a Cause. However, it would be brittle for client code
615 | // to rely on the arity of causes inside a MultiError, so we have opted not to
616 | // provide this functionality. Clients which really wish to access the Causes
617 | // of the underlying errors are free to iterate through the errors themselves.
618 | func (m MultiError) Cause() error { return nil }
619 |
620 | func (m MultiError) Error() string {
621 | s, n := "", 0
622 | for _, e := range m {
623 | if e != nil {
624 | if n == 0 {
625 | s = e.Error()
626 | }
627 | n++
628 | }
629 | }
630 | switch n {
631 | case 0:
632 | return "(0 errors)"
633 | case 1:
634 | return s
635 | case 2:
636 | return s + " (and 1 other error)"
637 | }
638 | return fmt.Sprintf("%s (and %d other errors)", s, n-1)
639 | }
640 |
641 | // any returns true if any element of m is an Error for which pred returns true.
642 | func (m MultiError) any(pred func(Error) bool) bool {
643 | for _, e := range m {
644 | if ourErr, ok := e.(Error); ok && pred(ourErr) {
645 | return true
646 | }
647 | }
648 | return false
649 | }
650 |
--------------------------------------------------------------------------------