├── .github ├── dependabot.yml └── workflows │ ├── bearer.yml │ ├── codeql.yml │ ├── go.yml │ └── goreleaser.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── example ├── code1 │ └── example.go └── code2 │ └── example.go ├── go.mod ├── go.sum ├── policy.go ├── secure.go └── secure_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/bearer.yml: -------------------------------------------------------------------------------- 1 | name: Bearer PR Check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | rule_check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - uses: reviewdog/action-setup@v1 19 | with: 20 | reviewdog_version: latest 21 | 22 | - name: Run Report 23 | id: report 24 | uses: bearer/bearer-action@v2 25 | with: 26 | format: rdjson 27 | output: rd.json 28 | diff: true 29 | 30 | - name: Run reviewdog 31 | if: always() 32 | env: 33 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | cat rd.json | reviewdog -f=rdjson -reporter=github-pr-review 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Setup golangci-lint 26 | uses: golangci/golangci-lint-action@v7 27 | with: 28 | version: v2.0 29 | args: --verbose 30 | test: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest, macos-latest] 34 | go: [1.23, 1.24] 35 | include: 36 | - os: ubuntu-latest 37 | go-build: ~/.cache/go-build 38 | - os: macos-latest 39 | go-build: ~/Library/Caches/go-build 40 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 41 | runs-on: ${{ matrix.os }} 42 | env: 43 | GO111MODULE: on 44 | GOPROXY: https://proxy.golang.org 45 | steps: 46 | - name: Set up Go ${{ matrix.go }} 47 | uses: actions/setup-go@v5 48 | with: 49 | go-version: ${{ matrix.go }} 50 | 51 | - name: Checkout Code 52 | uses: actions/checkout@v4 53 | with: 54 | ref: ${{ github.ref }} 55 | 56 | - uses: actions/cache@v4 57 | with: 58 | path: | 59 | ${{ matrix.go-build }} 60 | ~/go/pkg/mod 61 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 62 | restore-keys: | 63 | ${{ runner.os }}-go- 64 | - name: Run Tests 65 | run: | 66 | go test -v -covermode=atomic -coverprofile=coverage.out 67 | 68 | - name: Upload coverage to Codecov 69 | uses: codecov/codecov-action@v5 70 | with: 71 | flags: ${{ matrix.os }},go-${{ matrix.go }} 72 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | # either 'goreleaser' (default) or 'goreleaser-pro' 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - dogsled 7 | - dupl 8 | - errcheck 9 | - exhaustive 10 | - gochecknoinits 11 | - goconst 12 | - gocritic 13 | - gocyclo 14 | - goprintffuncname 15 | - gosec 16 | - govet 17 | - ineffassign 18 | - lll 19 | - misspell 20 | - nakedret 21 | - noctx 22 | - nolintlint 23 | - rowserrcheck 24 | - staticcheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - whitespace 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - gofumpt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | 4 | changelog: 5 | use: github 6 | groups: 7 | - title: Features 8 | regexp: "^.*feat[(\\w)]*:+.*$" 9 | order: 0 10 | - title: "Bug fixes" 11 | regexp: "^.*fix[(\\w)]*:+.*$" 12 | order: 1 13 | - title: "Enhancements" 14 | regexp: "^.*chore[(\\w)]*:+.*$" 15 | order: 2 16 | - title: "Refactor" 17 | regexp: "^.*refactor[(\\w)]*:+.*$" 18 | order: 3 19 | - title: "Build process updates" 20 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 21 | order: 4 22 | - title: "Documentation updates" 23 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: Others 26 | order: 999 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 gin-contrib 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure 2 | 3 | [![Run Tests](https://github.com/gin-contrib/secure/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/gin-contrib/secure/actions/workflows/go.yml) 4 | [![codecov](https://codecov.io/gh/gin-contrib/secure/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/secure) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/secure)](https://goreportcard.com/report/github.com/gin-contrib/secure) 6 | [![GoDoc](https://godoc.org/github.com/gin-contrib/secure?status.svg)](https://godoc.org/github.com/gin-contrib/secure) 7 | 8 | Secure middleware for [Gin](https://github.com/gin-gonic/gin/) framework. 9 | 10 | ## Example 11 | 12 | See the [example1](example/code1/example.go), [example2](example/code2/example.go). 13 | 14 | DefaultConfig returns a Configuration with strict security settings 15 | 16 | [embedmd]:# (secure.go go /func DefaultConfig/ /^}$/) 17 | ```go 18 | func DefaultConfig() Config { 19 | return Config{ 20 | SSLRedirect: true, 21 | IsDevelopment: false, 22 | STSSeconds: 315360000, 23 | STSIncludeSubdomains: true, 24 | FrameDeny: true, 25 | ContentTypeNosniff: true, 26 | BrowserXssFilter: true, 27 | ContentSecurityPolicy: "default-src 'self'", 28 | IENoOpen: true, 29 | SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, 30 | } 31 | } 32 | ``` 33 | 34 | [embedmd]:# (example/code1/example.go go) 35 | ```go 36 | package main 37 | 38 | import ( 39 | "log" 40 | 41 | "github.com/gin-contrib/secure" 42 | "github.com/gin-gonic/gin" 43 | ) 44 | 45 | func main() { 46 | router := gin.Default() 47 | 48 | router.Use(secure.New(secure.Config{ 49 | AllowedHosts: []string{"example.com", "ssl.example.com"}, 50 | SSLRedirect: true, 51 | SSLHost: "ssl.example.com", 52 | STSSeconds: 315360000, 53 | STSIncludeSubdomains: true, 54 | FrameDeny: true, 55 | ContentTypeNosniff: true, 56 | BrowserXssFilter: true, 57 | ContentSecurityPolicy: "default-src 'self'", 58 | IENoOpen: true, 59 | ReferrerPolicy: "strict-origin-when-cross-origin", 60 | SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, 61 | })) 62 | 63 | router.GET("/ping", func(c *gin.Context) { 64 | c.String(200, "pong") 65 | }) 66 | 67 | // Listen and Server in 0.0.0.0:8080 68 | if err := router.Run(); err != nil { 69 | log.Fatal(err) 70 | } 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /example/code1/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/gin-contrib/secure" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | router := gin.Default() 12 | 13 | router.Use(secure.New(secure.Config{ 14 | AllowedHosts: []string{"example.com", "ssl.example.com"}, 15 | SSLRedirect: true, 16 | SSLHost: "ssl.example.com", 17 | STSSeconds: 315360000, 18 | STSIncludeSubdomains: true, 19 | FrameDeny: true, 20 | ContentTypeNosniff: true, 21 | BrowserXssFilter: true, 22 | ContentSecurityPolicy: "default-src 'self'", 23 | IENoOpen: true, 24 | ReferrerPolicy: "strict-origin-when-cross-origin", 25 | SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, 26 | })) 27 | 28 | router.GET("/ping", func(c *gin.Context) { 29 | c.String(200, "pong") 30 | }) 31 | 32 | // Listen and Server in 0.0.0.0:8080 33 | if err := router.Run(); err != nil { 34 | log.Fatal(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/code2/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/gin-contrib/secure" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | router := gin.Default() 12 | 13 | securityConfig := secure.DefaultConfig() 14 | securityConfig.AllowedHosts = []string{"example.com", "ssl.example.com"} 15 | securityConfig.SSLHost = "ssl.example.com" 16 | router.Use(secure.New(securityConfig)) 17 | 18 | router.GET("/ping", func(c *gin.Context) { 19 | c.String(200, "pong") 20 | }) 21 | 22 | if err := router.Run(); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gin-contrib/secure 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.2 // indirect 12 | github.com/bytedance/sonic/loader v0.2.4 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 16 | github.com/gin-contrib/sse v1.0.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.26.0 // indirect 20 | github.com/goccy/go-json v0.10.5 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 23 | github.com/kr/pretty v0.3.0 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/rogpeppe/go-internal v1.8.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.12 // indirect 33 | golang.org/x/arch v0.16.0 // indirect 34 | golang.org/x/crypto v0.37.0 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/sys v0.32.0 // indirect 37 | golang.org/x/text v0.24.0 // indirect 38 | google.golang.org/protobuf v1.36.6 // indirect 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 2 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 5 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 14 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 15 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 16 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 17 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 18 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 19 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 20 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 21 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 22 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 23 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 24 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 25 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 26 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 27 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 28 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 29 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 34 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 35 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 36 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 37 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 38 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 39 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 40 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 41 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 42 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 43 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 44 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 45 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 46 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 47 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 48 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 49 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 54 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 55 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 56 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 57 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 61 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 62 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 65 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 66 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 67 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 68 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 69 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 71 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 72 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 73 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 74 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 75 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 76 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 77 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 78 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 79 | golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= 80 | golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 81 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 82 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 83 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 84 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 85 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 87 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 88 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 89 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 90 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 93 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 97 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 98 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 101 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 103 | -------------------------------------------------------------------------------- /policy.go: -------------------------------------------------------------------------------- 1 | package secure 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type ( 13 | // Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be 14 | // provided to configure which features should be enabled, and the ability to override a few of the default values. 15 | policy struct { 16 | // Customize Secure with an Options struct. 17 | config Config 18 | fixedHeaders []header 19 | } 20 | 21 | header struct { 22 | key string 23 | value []string 24 | } 25 | ) 26 | 27 | // Constructs a new Policy instance with supplied options. 28 | func newPolicy(config Config) *policy { 29 | policy := &policy{} 30 | policy.loadConfig(config) 31 | return policy 32 | } 33 | 34 | func (p *policy) loadConfig(config Config) { 35 | p.config = config 36 | p.fixedHeaders = make([]header, 0, 5) 37 | 38 | // Frame Options header. 39 | if len(config.CustomFrameOptionsValue) > 0 { 40 | p.addHeader("X-Frame-Options", config.CustomFrameOptionsValue) 41 | } else if config.FrameDeny { 42 | p.addHeader("X-Frame-Options", "DENY") 43 | } 44 | 45 | // Content Type Options header. 46 | if config.ContentTypeNosniff { 47 | p.addHeader("X-Content-Type-Options", "nosniff") 48 | } 49 | 50 | // XSS Protection header. 51 | if config.BrowserXssFilter { 52 | p.addHeader("X-Xss-Protection", "1; mode=block") 53 | } 54 | 55 | // Content Security Policy header. 56 | if len(config.ContentSecurityPolicy) > 0 { 57 | p.addHeader("Content-Security-Policy", config.ContentSecurityPolicy) 58 | } 59 | 60 | if len(config.ReferrerPolicy) > 0 { 61 | p.addHeader("Referrer-Policy", config.ReferrerPolicy) 62 | } 63 | 64 | // Strict Transport Security header. 65 | if config.STSSeconds != 0 { 66 | stsSub := "" 67 | if config.STSIncludeSubdomains { 68 | stsSub = "; includeSubdomains" 69 | } 70 | if config.STSPreload { 71 | stsSub = "; preload" 72 | } 73 | // TODO 74 | // "max-age=%d%s" refactor 75 | p.addHeader( 76 | "Strict-Transport-Security", 77 | fmt.Sprintf("max-age=%d%s", config.STSSeconds, stsSub)) 78 | } 79 | 80 | // X-Download-Options header. 81 | if config.IENoOpen { 82 | p.addHeader("X-Download-Options", "noopen") 83 | } 84 | 85 | // FeaturePolicy header. 86 | if len(config.FeaturePolicy) > 0 { 87 | p.addHeader("Feature-Policy", config.FeaturePolicy) 88 | } 89 | } 90 | 91 | func (p *policy) addHeader(key string, value string) { 92 | p.fixedHeaders = append(p.fixedHeaders, header{ 93 | key: key, 94 | value: []string{value}, 95 | }) 96 | } 97 | 98 | func (p *policy) applyToContext(c *gin.Context) bool { 99 | if !p.config.IsDevelopment { 100 | p.writeSecureHeaders(c) 101 | 102 | if !p.checkAllowHosts(c) { 103 | return false 104 | } 105 | if !p.checkSSL(c) { 106 | return false 107 | } 108 | } 109 | return true 110 | } 111 | 112 | func (p *policy) writeSecureHeaders(c *gin.Context) { 113 | header := c.Writer.Header() 114 | for _, pair := range p.fixedHeaders { 115 | header[pair.key] = pair.value 116 | } 117 | } 118 | 119 | func (p *policy) checkAllowHosts(c *gin.Context) bool { 120 | if len(p.config.AllowedHosts) == 0 { 121 | return true 122 | } 123 | 124 | host := c.Request.Host 125 | if len(host) == 0 { 126 | host = c.Request.URL.Host 127 | } 128 | 129 | for _, allowedHost := range p.config.AllowedHosts { 130 | if strings.EqualFold(allowedHost, host) { 131 | return true 132 | } 133 | } 134 | 135 | if p.config.BadHostHandler != nil { 136 | p.config.BadHostHandler(c) 137 | } else { 138 | c.AbortWithStatus(403) 139 | } 140 | 141 | return false 142 | } 143 | 144 | // checks if a host (possibly with trailing port) is an IPV4 address 145 | func isIPV4(host string) bool { 146 | if index := strings.IndexByte(host, ':'); index != -1 { 147 | host = host[:index] 148 | } 149 | return net.ParseIP(host) != nil 150 | } 151 | 152 | func (p *policy) isSSLRequest(req *http.Request) bool { 153 | if strings.EqualFold(req.URL.Scheme, "https") || req.TLS != nil { 154 | return true 155 | } 156 | 157 | for h, v := range p.config.SSLProxyHeaders { 158 | hv, ok := req.Header[h] 159 | 160 | if !ok { 161 | continue 162 | } 163 | 164 | if strings.EqualFold(hv[0], v) { 165 | return true 166 | } 167 | } 168 | 169 | if p.config.DontRedirectIPV4Hostnames && isIPV4(req.Host) { 170 | return true 171 | } 172 | 173 | return false 174 | } 175 | 176 | func (p *policy) checkSSL(c *gin.Context) bool { 177 | if !p.config.SSLRedirect { 178 | return true 179 | } 180 | 181 | req := c.Request 182 | isSSLRequest := p.isSSLRequest(req) 183 | if isSSLRequest { 184 | return true 185 | } 186 | 187 | // TODO 188 | // req.Host vs req.URL.Host 189 | url := req.URL 190 | url.Scheme = "https" 191 | url.Host = req.Host 192 | 193 | if len(p.config.SSLHost) > 0 { 194 | url.Host = p.config.SSLHost 195 | } 196 | 197 | status := http.StatusMovedPermanently 198 | if p.config.SSLTemporaryRedirect { 199 | status = http.StatusTemporaryRedirect 200 | } 201 | c.Redirect(status, url.String()) 202 | c.Abort() 203 | return false 204 | } 205 | -------------------------------------------------------------------------------- /secure.go: -------------------------------------------------------------------------------- 1 | package secure 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // Config is a struct for specifying configuration options for the secure. 6 | type Config struct { 7 | // AllowedHosts is a list of fully qualified domain names that are allowed. 8 | // Default is empty list, which allows any and all host names. 9 | AllowedHosts []string 10 | // If SSLRedirect is set to true, then only allow https requests. 11 | // Default is false. 12 | SSLRedirect bool 13 | // If SSLTemporaryRedirect is true, the a 302 will be used while redirecting. 14 | // Default is false (301). 15 | SSLTemporaryRedirect bool 16 | // SSLHost is the host name that is used to redirect http requests to https. 17 | // Default is "", which indicates to use the same host. 18 | SSLHost string 19 | // STSSeconds is the max-age of the Strict-Transport-Security header. 20 | // Default is 0, which would NOT include the header. 21 | STSSeconds int64 22 | // If STSIncludeSubdomains is set to true, the `includeSubdomains` will 23 | // be appended to the Strict-Transport-Security header. Default is false. 24 | STSIncludeSubdomains bool 25 | // If STSPreload is set to true, the `; preload` will be appended to the 26 | // Strict-Transport-Security header. Default is false. 27 | // Note that removal is non-trivial and enabling this means you need to 28 | // support https long-term. See https://hstspreload.org/ for more info. 29 | STSPreload bool 30 | // If FrameDeny is set to true, adds the X-Frame-Options header with 31 | // the value of `DENY`. Default is false. 32 | FrameDeny bool 33 | // CustomFrameOptionsValue allows the X-Frame-Options header value 34 | // to be set with a custom value. This overrides the FrameDeny option. 35 | CustomFrameOptionsValue string 36 | // If ContentTypeNosniff is true, adds the X-Content-Type-Options header 37 | // with the value `nosniff`. Default is false. 38 | ContentTypeNosniff bool 39 | // If BrowserXssFilter is true, adds the X-XSS-Protection header with 40 | // the value `1; mode=block`. Default is false. 41 | BrowserXssFilter bool 42 | // ContentSecurityPolicy allows the Content-Security-Policy header value 43 | // to be set with a custom value. Default is "". 44 | ContentSecurityPolicy string 45 | // HTTP header "Referrer-Policy" governs which referrer information, sent in the Referrer header, 46 | // should be included with requests made. 47 | ReferrerPolicy string 48 | // When true, the whole security policy applied by the middleware is disabled completely. 49 | IsDevelopment bool 50 | // Handlers for when an error occurs (ie bad host). 51 | BadHostHandler gin.HandlerFunc 52 | // Prevent Internet Explorer from executing downloads in your site’s context 53 | IENoOpen bool 54 | // Feature Policy is a new header that allows a site to control which features and APIs can be used in the browser. 55 | FeaturePolicy string 56 | // If DontRedirectIPV4Hostnames is true, requests to hostnames that are IPV4 57 | // addresses aren't redirected. This is to allow load balancer health checks 58 | // to succeed. 59 | DontRedirectIPV4Hostnames bool 60 | 61 | // If the request is insecure, treat it as secure if any of the headers 62 | // in this dict are set to their corresponding value. 63 | // This is useful when your app is running behind a secure proxy that forwards requests to your app over http 64 | // (such as on Heroku). 65 | SSLProxyHeaders map[string]string 66 | } 67 | 68 | // DefaultConfig returns a Configuration with strict security settings. 69 | // ``` 70 | // 71 | // SSLRedirect: true 72 | // IsDevelopment: false 73 | // STSSeconds: 315360000 74 | // STSIncludeSubdomains: true 75 | // FrameDeny: true 76 | // ContentTypeNosniff: true 77 | // BrowserXssFilter: true 78 | // ContentSecurityPolicy: "default-src 'self'" 79 | // SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, 80 | // 81 | // ``` 82 | func DefaultConfig() Config { 83 | return Config{ 84 | SSLRedirect: true, 85 | IsDevelopment: false, 86 | STSSeconds: 315360000, 87 | STSIncludeSubdomains: true, 88 | STSPreload: true, 89 | FrameDeny: true, 90 | ContentTypeNosniff: true, 91 | BrowserXssFilter: true, 92 | ContentSecurityPolicy: "default-src 'self'", 93 | IENoOpen: true, 94 | SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, 95 | } 96 | } 97 | 98 | // New creates an instance of the secure middleware using the specified configuration. 99 | // router.Use(secure.N) 100 | func New(config Config) gin.HandlerFunc { 101 | policy := newPolicy(config) 102 | return func(c *gin.Context) { 103 | if !policy.applyToContext(c) { 104 | return 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /secure_test.go: -------------------------------------------------------------------------------- 1 | package secure 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ( 14 | testResponse = "bar" 15 | exampleHost = "www.example.com" 16 | httpScheme = "http" 17 | ) 18 | 19 | func newServer(options Config) *gin.Engine { 20 | router := gin.New() 21 | router.Use(New(options)) 22 | router.GET("/foo", func(c *gin.Context) { 23 | c.String(200, testResponse) 24 | }) 25 | return router 26 | } 27 | 28 | func performRequest(router *gin.Engine, path string) *httptest.ResponseRecorder { 29 | w := httptest.NewRecorder() 30 | req, _ := http.NewRequestWithContext(context.Background(), "GET", path, nil) 31 | router.ServeHTTP(w, req) 32 | return w 33 | } 34 | 35 | func TestNoConfig(t *testing.T) { 36 | router := newServer(Config{ 37 | // Intentionally left blank. 38 | }) 39 | 40 | w := performRequest(router, "http://example.com/foo") 41 | 42 | assert.Equal(t, http.StatusOK, w.Code) 43 | assert.Equal(t, "bar", w.Body.String()) 44 | } 45 | 46 | func TestDefaultConfig(t *testing.T) { 47 | router := newServer(DefaultConfig()) 48 | 49 | w := performRequest(router, "https://"+exampleHost+"/foo") 50 | 51 | assert.Equal(t, http.StatusOK, w.Code) 52 | assert.Equal(t, "bar", w.Body.String()) 53 | 54 | w = performRequest(router, "http://"+exampleHost+"/foo") 55 | 56 | assert.Equal(t, http.StatusMovedPermanently, w.Code) 57 | assert.Equal(t, "https://www.example.com/foo", w.Header().Get("Location")) 58 | } 59 | 60 | func TestNoAllowHosts(t *testing.T) { 61 | router := newServer(Config{ 62 | AllowedHosts: []string{}, 63 | }) 64 | 65 | w := performRequest(router, "http://"+exampleHost+"/foo") 66 | 67 | assert.Equal(t, http.StatusOK, w.Code) 68 | assert.Equal(t, "bar", w.Body.String()) 69 | } 70 | 71 | func TestGoodSingleAllowHosts(t *testing.T) { 72 | router := newServer(Config{ 73 | AllowedHosts: []string{exampleHost}, 74 | }) 75 | 76 | w := performRequest(router, "http://"+exampleHost+"/foo") 77 | 78 | assert.Equal(t, http.StatusOK, w.Code) 79 | assert.Equal(t, "bar", w.Body.String()) 80 | } 81 | 82 | func TestBadSingleAllowHosts(t *testing.T) { 83 | router := newServer(Config{ 84 | AllowedHosts: []string{"sub.example.com"}, 85 | }) 86 | 87 | w := performRequest(router, "http://www.example.com/foo") 88 | 89 | assert.Equal(t, http.StatusForbidden, w.Code) 90 | } 91 | 92 | func TestGoodMultipleAllowHosts(t *testing.T) { 93 | router := newServer(Config{ 94 | AllowedHosts: []string{exampleHost, "sub.example.com"}, 95 | }) 96 | 97 | w := performRequest(router, "http://sub.example.com/foo") 98 | 99 | assert.Equal(t, http.StatusOK, w.Code) 100 | assert.Equal(t, "bar", w.Body.String()) 101 | } 102 | 103 | func TestBadMultipleAllowHosts(t *testing.T) { 104 | router := newServer(Config{ 105 | AllowedHosts: []string{"www.example.com", "sub.example.com"}, 106 | }) 107 | 108 | w := performRequest(router, "http://www3.example.com/foo") 109 | 110 | assert.Equal(t, http.StatusForbidden, w.Code) 111 | } 112 | 113 | func TestAllowHostsInDevMode(t *testing.T) { 114 | router := newServer(Config{ 115 | AllowedHosts: []string{"www.example.com", "sub.example.com"}, 116 | IsDevelopment: true, 117 | }) 118 | 119 | w := performRequest(router, "http://www3.example.com/foo") 120 | 121 | assert.Equal(t, http.StatusOK, w.Code) 122 | } 123 | 124 | func TestBadHostHandler(t *testing.T) { 125 | badHandler := func(c *gin.Context) { 126 | c.String(http.StatusInternalServerError, "BadHost") 127 | c.Abort() 128 | } 129 | 130 | router := newServer(Config{ 131 | AllowedHosts: []string{"www.example.com", "sub.example.com"}, 132 | BadHostHandler: badHandler, 133 | }) 134 | 135 | w := performRequest(router, "http://www3.example.com/foo") 136 | 137 | assert.Equal(t, http.StatusInternalServerError, w.Code) 138 | assert.Equal(t, "BadHost", w.Body.String()) 139 | } 140 | 141 | func TestSSL(t *testing.T) { 142 | router := newServer(Config{ 143 | SSLRedirect: true, 144 | }) 145 | 146 | w := performRequest(router, "https://www.example.com/foo") 147 | 148 | assert.Equal(t, http.StatusOK, w.Code) 149 | assert.Equal(t, "bar", w.Body.String()) 150 | } 151 | 152 | func TestSSLInDevMode(t *testing.T) { 153 | router := newServer(Config{ 154 | SSLRedirect: true, 155 | IsDevelopment: true, 156 | }) 157 | 158 | w := performRequest(router, "http://www.example.com/foo") 159 | 160 | assert.Equal(t, http.StatusOK, w.Code) 161 | assert.Equal(t, "bar", w.Body.String()) 162 | } 163 | 164 | func TestBasicSSL(t *testing.T) { 165 | router := newServer(Config{ 166 | SSLRedirect: true, 167 | }) 168 | 169 | w := performRequest(router, "http://www.example.com/foo") 170 | 171 | assert.Equal(t, http.StatusMovedPermanently, w.Code) 172 | assert.Equal(t, "https://www.example.com/foo", w.Header().Get("Location")) 173 | } 174 | 175 | func TestDontRedirectIPV4Hostnames(t *testing.T) { 176 | router := newServer(Config{ 177 | SSLRedirect: true, 178 | DontRedirectIPV4Hostnames: true, 179 | }) 180 | 181 | w1 := performRequest(router, "http://"+exampleHost+"/foo") 182 | assert.Equal(t, http.StatusMovedPermanently, w1.Code) 183 | 184 | w2 := performRequest(router, "http://127.0.0.1/foo") 185 | assert.Equal(t, http.StatusOK, w2.Code) 186 | } 187 | 188 | func TestBasicSSLWithHost(t *testing.T) { 189 | router := newServer(Config{ 190 | SSLRedirect: true, 191 | SSLHost: "secure.example.com", 192 | }) 193 | 194 | w := performRequest(router, "http://"+exampleHost+"/foo") 195 | 196 | assert.Equal(t, http.StatusMovedPermanently, w.Code) 197 | assert.Equal(t, "https://secure.example.com/foo", w.Header().Get("Location")) 198 | } 199 | 200 | func TestBadProxySSL(t *testing.T) { 201 | router := newServer(Config{ 202 | SSLRedirect: true, 203 | }) 204 | 205 | w := httptest.NewRecorder() 206 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/foo", nil) 207 | req.Host = exampleHost 208 | req.URL.Scheme = httpScheme 209 | req.Header.Add("X-Forwarded-Proto", "https") 210 | 211 | router.ServeHTTP(w, req) 212 | 213 | assert.Equal(t, http.StatusMovedPermanently, w.Code) 214 | assert.Equal(t, "https://www.example.com/foo", w.Header().Get("Location")) 215 | } 216 | 217 | func TestProxySSLWithHeaderOption(t *testing.T) { 218 | router := newServer(Config{ 219 | SSLRedirect: true, 220 | SSLProxyHeaders: map[string]string{"X-Arbitrary-Header": "arbitrary-value"}, 221 | }) 222 | 223 | w := httptest.NewRecorder() 224 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/foo", nil) 225 | req.Host = exampleHost 226 | req.URL.Scheme = "http" 227 | req.Header.Add("X-Arbitrary-Header", "arbitrary-value") 228 | 229 | router.ServeHTTP(w, req) 230 | 231 | assert.Equal(t, http.StatusOK, w.Code) 232 | } 233 | 234 | func TestProxySSLWithWrongHeaderValue(t *testing.T) { 235 | router := newServer(Config{ 236 | SSLRedirect: true, 237 | SSLProxyHeaders: map[string]string{"X-Arbitrary-Header": "arbitrary-value"}, 238 | }) 239 | 240 | w := httptest.NewRecorder() 241 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/foo", nil) 242 | req.Host = "www.example.com" 243 | req.URL.Scheme = "http" 244 | req.Header.Add("X-Arbitrary-Header", "wrong-value") 245 | 246 | router.ServeHTTP(w, req) 247 | 248 | assert.Equal(t, http.StatusMovedPermanently, w.Code) 249 | assert.Equal(t, "https://www.example.com/foo", w.Header().Get("Location")) 250 | } 251 | 252 | func TestStsHeader(t *testing.T) { 253 | router := newServer(Config{ 254 | STSSeconds: 315360000, 255 | }) 256 | 257 | w := performRequest(router, "/foo") 258 | 259 | assert.Equal(t, http.StatusOK, w.Code) 260 | assert.Equal(t, "max-age=315360000", w.Header().Get("Strict-Transport-Security")) 261 | } 262 | 263 | func TestStsHeaderInDevMode(t *testing.T) { 264 | router := newServer(Config{ 265 | STSSeconds: 315360000, 266 | IsDevelopment: true, 267 | }) 268 | 269 | w := performRequest(router, "/foo") 270 | 271 | assert.Equal(t, http.StatusOK, w.Code) 272 | assert.Equal(t, "", w.Header().Get("Strict-Transport-Security")) 273 | } 274 | 275 | func TestStsHeaderWithSubdomain(t *testing.T) { 276 | router := newServer(Config{ 277 | STSSeconds: 315360000, 278 | STSIncludeSubdomains: true, 279 | }) 280 | 281 | w := performRequest(router, "/foo") 282 | 283 | assert.Equal(t, http.StatusOK, w.Code) 284 | assert.Equal(t, "max-age=315360000; includeSubdomains", w.Header().Get("Strict-Transport-Security")) 285 | } 286 | 287 | func TestFrameDeny(t *testing.T) { 288 | router := newServer(Config{ 289 | FrameDeny: true, 290 | }) 291 | 292 | w := performRequest(router, "/foo") 293 | 294 | assert.Equal(t, http.StatusOK, w.Code) 295 | assert.Equal(t, "DENY", w.Header().Get("X-Frame-Options")) 296 | } 297 | 298 | func TestCustomFrameValue(t *testing.T) { 299 | router := newServer(Config{ 300 | CustomFrameOptionsValue: "SAMEORIGIN", 301 | }) 302 | 303 | w := performRequest(router, "/foo") 304 | 305 | assert.Equal(t, http.StatusOK, w.Code) 306 | assert.Equal(t, "SAMEORIGIN", w.Header().Get("X-Frame-Options")) 307 | } 308 | 309 | func TestCustomFrameValueWithDeny(t *testing.T) { 310 | router := newServer(Config{ 311 | FrameDeny: true, 312 | CustomFrameOptionsValue: "SAMEORIGIN", 313 | }) 314 | 315 | w := performRequest(router, "/foo") 316 | 317 | assert.Equal(t, http.StatusOK, w.Code) 318 | assert.Equal(t, "SAMEORIGIN", w.Header().Get("X-Frame-Options")) 319 | } 320 | 321 | func TestContentNosniff(t *testing.T) { 322 | router := newServer(Config{ 323 | ContentTypeNosniff: true, 324 | }) 325 | 326 | w := performRequest(router, "/foo") 327 | 328 | assert.Equal(t, http.StatusOK, w.Code) 329 | assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) 330 | } 331 | 332 | func TestXSSProtection(t *testing.T) { 333 | router := newServer(Config{ 334 | BrowserXssFilter: true, 335 | }) 336 | 337 | w := performRequest(router, "/foo") 338 | 339 | assert.Equal(t, http.StatusOK, w.Code) 340 | assert.Equal(t, "1; mode=block", w.Header().Get("X-XSS-Protection")) 341 | } 342 | 343 | func TestReferrerPolicy(t *testing.T) { 344 | router := newServer(Config{ 345 | ReferrerPolicy: "strict-origin-when-cross-origin", 346 | }) 347 | 348 | w := performRequest(router, "/foo") 349 | 350 | assert.Equal(t, http.StatusOK, w.Code) 351 | assert.Equal(t, "strict-origin-when-cross-origin", w.Header().Get("Referrer-Policy")) 352 | } 353 | 354 | func TestFeaturePolicy(t *testing.T) { 355 | router := newServer(Config{ 356 | FeaturePolicy: "vibrate 'none';", 357 | }) 358 | 359 | w := performRequest(router, "/foo") 360 | 361 | assert.Equal(t, http.StatusOK, w.Code) 362 | assert.Equal(t, "vibrate 'none';", w.Header().Get("Feature-Policy")) 363 | } 364 | 365 | func TestCsp(t *testing.T) { 366 | router := newServer(Config{ 367 | ContentSecurityPolicy: "default-src 'self'", 368 | }) 369 | 370 | w := performRequest(router, "/foo") 371 | 372 | assert.Equal(t, http.StatusOK, w.Code) 373 | assert.Equal(t, "default-src 'self'", w.Header().Get("Content-Security-Policy")) 374 | } 375 | 376 | func TestInlineSecure(t *testing.T) { 377 | router := newServer(Config{ 378 | FrameDeny: true, 379 | }) 380 | 381 | w := performRequest(router, "/foo") 382 | 383 | assert.Equal(t, http.StatusOK, w.Code) 384 | assert.Equal(t, "DENY", w.Header().Get("X-Frame-Options")) 385 | } 386 | 387 | func TestIsIpv4Host(t *testing.T) { 388 | assert.Equal(t, isIPV4("127.0.0.1"), true) 389 | assert.Equal(t, isIPV4("127.0.0.1:8080"), true) 390 | assert.Equal(t, isIPV4("localhost"), false) 391 | assert.Equal(t, isIPV4("localhost:8080"), false) 392 | assert.Equal(t, isIPV4("example.com"), false) 393 | assert.Equal(t, isIPV4("example.com:8080"), false) 394 | } 395 | --------------------------------------------------------------------------------