├── .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 | ![testing](https://github.com/gorilla/securecookie/actions/workflows/test.yml/badge.svg) 4 | [![codecov](https://codecov.io/github/gorilla/securecookie/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/securecookie) 5 | [![godoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) 6 | [![sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge) 7 | 8 | ![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) 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 |
54 | 55 | 56 | 61 | {{ .csrfField }} 62 | 63 |
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 |
23 | {{ .csrfField }} 24 |
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 | ![testing](https://github.com/gorilla/csrf/actions/workflows/test.yml/badge.svg) 4 | [![codecov](https://codecov.io/github/gorilla/csrf/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/csrf) 5 | [![godoc](https://godoc.org/github.com/gorilla/csrf?status.svg)](https://godoc.org/github.com/gorilla/csrf) 6 | [![sourcegraph](https://sourcegraph.com/github.com/gorilla/csrf/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/csrf?badge) 7 | 8 | 9 | ![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) 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 | --------------------------------------------------------------------------------