forked from unrolled/secure
/
secure.go
299 lines (249 loc) · 7.89 KB
/
secure.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// Package secure is an HTTP middleware for Go that handles adding security
// headers to HTTP responses, and accompanying security checks.
//
// package main
//
// import (
// "net/http"
//
// "github.com/kenshaw/secure"
// )
//
// var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte("hello world"))
// })
//
// func main() {
// secureMiddleware := secure.New(
// secure.AllowedHosts("www.example.com", "sub.example.com"),
// secure.SSLRedirect(true),
// })
//
// app := secureMiddleware.Handler(myHandler)
// http.ListenAndServe("127.0.0.1:3000", app)
// }
package secure
//go:generate go run gen-readme.go > README.md
import (
"fmt"
"net/http"
"strings"
)
// Error is a secure error.
type Error string
// Error satisfies the error interface.
func (err Error) Error() string {
return string(err)
}
// Error values.
const (
// ErrBadHost is the bad host error.
ErrBadHost Error = "bad host"
// ErrHTTPSRedirect is the https redirect error.
ErrHTTPSRedirect Error = "https redirect"
)
// Middleware that sets basic security headers and provides simple security
// checks for http servers.
type Middleware struct {
// AllowedHosts is a list of fully qualified domain names that are allowed.
// When empty, allows any host.
AllowedHosts []string
// HostsProxyHeaders is a set of header keys that may hold a proxied
// hostname value for the request.
HostsProxyHeaders []string
// If SSLRedirect is set to true, then only allow https requests.
SSLRedirect bool
// If SSLTemporaryRedirect is true, the a 302 will be used while
// redirecting.
SSLTemporaryRedirect bool
// SSLHost is the host name that is used to redirect http requests to
// https. If not set, indicates to use the same host.
SSLHost string
// SSLForwardedProxyHeaders is the set of header keys with associated
// values that would indicate a valid https request. This is used when
// proxying requests from behind another webserver (ie, nginx, apache,
// etc).
//
// &secure.Middleware{
// SSLForwardedProxyHeaders: map[string]string{
// "X-Forwarded-Proto": "https",
// },
// }
//
SSLForwardedProxyHeaders map[string]string
// STSSeconds is the max-age of the Strict-Transport-Security header.
// Header will not be included if STSSeconds = 0.
STSSeconds int64
// When STSIncludeSubdomains is true, `includeSubdomains` will be appended to
// the Strict-Transport-Security header.
STSIncludeSubdomains bool
// When STSPreload is true, the `preload` flag will be appended to the
// Strict-Transport-Security header.
STSPreload bool
// When ForceSTSHeader is true, the STS header will be added even when the
// connection is HTTP.
ForceSTSHeader bool
// When FrameDeny is true, adds the X-Frame-Options header with the value
// of `DENY`.
FrameDeny bool
// CustomFrameOptionsValue allows the X-Frame-Options header value to be
// set with a custom value. Overrides the FrameDeny option.
CustomFrameOptionsValue string
// If ContentTypeNosniff is true, adds the X-Content-Type-Options header
// with the value `nosniff`.
ContentTypeNosniff bool
// If BrowserXSSFilter is true, adds the X-XSS-Protection header with the
// value `1; mode=block`.
BrowserXSSFilter bool
// CustomBrowserXSSValue allows the X-XSS-Protection header value to be set
// with a custom value. This overrides the BrowserXSSFilter option.
CustomBrowserXSSValue string
// ContentSecurityPolicy allows the Content-Security-Policy header value to
// be set with a custom value.
ContentSecurityPolicy string
// ReferrerPolicy configures which the browser referrer policy.
ReferrerPolicy string
// BadHostHandler is the bad host handler.
BadHostHandler http.HandlerFunc
// When DevEnvironment is true, disables the AllowedHosts, SSL, and STS
// checks.
//
// This should be toggled only when testing / developing, and is necessary
// when testing sites configured only for https from a http based
// connection.
//
// If you would like your development environment to mimic production with
// complete Host blocking, SSL redirects, and STS headers, leave this as
// false.
DevEnvironment bool
}
// New constructs a new secure Middleware instance with the supplied options.
func New(opts ...Option) *Middleware {
s := &Middleware{
BadHostHandler: DefaultBadHostHandler,
}
// apply opts
for _, o := range opts {
o(s)
}
return s
}
// Handler implements the http.HandlerFunc for integration with the standard
// net/http lib.
func (s *Middleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// process the request. if an error is returned, then bail (ie, request
// should not continue down handler chain)
if err := s.Process(w, r); err != nil {
return
}
h.ServeHTTP(w, r)
})
}
// HandlerFuncWithNext is a special implementation for Negroni, but could be
// used elsewhere.
func (s *Middleware) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
err := s.Process(w, r)
// if there was an error, do not call next
if err == nil && next != nil {
next(w, r)
}
}
// Process runs the actual checks and returns an error if the middleware chain
// should stop.
func (s *Middleware) Process(w http.ResponseWriter, r *http.Request) error {
// resolve the host for the request, using proxy headers if present
host := r.Host
for _, header := range s.HostsProxyHeaders {
if h := r.Header.Get(header); h != "" {
host = h
break
}
}
// allowed hosts check
if len(s.AllowedHosts) > 0 && !s.DevEnvironment {
var isGoodHost bool
for _, allowedHost := range s.AllowedHosts {
if strings.EqualFold(allowedHost, host) {
isGoodHost = true
break
}
}
if !isGoodHost {
if s.BadHostHandler != nil {
s.BadHostHandler(w, r)
} else {
DefaultBadHostHandler(w, r)
}
return ErrBadHost
}
}
// determine if we are on https
isSSL := r.URL.Scheme == "https" || r.TLS != nil
if !isSSL {
for k, v := range s.SSLForwardedProxyHeaders {
if r.Header.Get(k) == v {
isSSL = true
break
}
}
}
// ssl check
if s.SSLRedirect && !isSSL && !s.DevEnvironment {
url := r.URL
url.Scheme = "https"
url.Host = host
if len(s.SSLHost) > 0 {
url.Host = s.SSLHost
}
status := http.StatusMovedPermanently
if s.SSLTemporaryRedirect {
status = http.StatusTemporaryRedirect
}
http.Redirect(w, r, url.String(), status)
return ErrHTTPSRedirect
}
// only add Strict-Transport-Security header when we know it's an ssl
// connection
//
// see: https://tools.ietf.org/html/rfc6797#section-7.2
if s.STSSeconds != 0 && (isSSL || s.ForceSTSHeader) && !s.DevEnvironment {
var stsSub string
if s.STSIncludeSubdomains {
stsSub = "; includeSubdomains"
}
if s.STSPreload {
stsSub += "; preload"
}
w.Header().Add("Strict-Transport-Security", fmt.Sprintf("max-age=%d%s", s.STSSeconds, stsSub))
}
// frame options
if len(s.CustomFrameOptionsValue) > 0 {
w.Header().Add("X-Frame-Options", s.CustomFrameOptionsValue)
} else if s.FrameDeny {
w.Header().Add("X-Frame-Options", "DENY")
}
// content type options
if s.ContentTypeNosniff {
w.Header().Add("X-Content-Type-Options", "nosniff")
}
// xss protection
if len(s.CustomBrowserXSSValue) > 0 {
w.Header().Add("X-XSS-Protection", s.CustomBrowserXSSValue)
} else if s.BrowserXSSFilter {
w.Header().Add("X-XSS-Protection", "1; mode=block")
}
// content security policy
if len(s.ContentSecurityPolicy) > 0 {
w.Header().Add("Content-Security-Policy", s.ContentSecurityPolicy)
}
// referrer policy
if len(s.ReferrerPolicy) > 0 {
w.Header().Add("Referrer-Policy", s.ReferrerPolicy)
}
return nil
}
// DefaultBadHostHandler is the default bad host http handler.
func DefaultBadHostHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad host", http.StatusInternalServerError)
}