Use responseModifier to override secure headers

This commit is contained in:
Michael 2018-03-01 16:42:04 +01:00 committed by Traefiker Bot
parent 831a3e384b
commit c77fe6b434
6 changed files with 348 additions and 37 deletions

5
Gopkg.lock generated
View file

@ -1163,9 +1163,10 @@
revision = "50716a0a853771bb36bfce61a45cdefdb98c2e6e"
[[projects]]
branch = "v1"
name = "github.com/unrolled/secure"
packages = ["."]
revision = "824e85271811af89640ea25620c67f6c2eed987e"
revision = "88720c9cbecfcf2eceb9bb4311ad6e398a8381ed"
[[projects]]
name = "github.com/urfave/negroni"
@ -1564,6 +1565,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "b31bcfd5f1894d3ce3c0468cd7273f89c759fc384254de74548c5cbe219c23e9"
inputs-digest = "b748d60cfaddd7ff38c63bdf1004ae7dc2dcd084b220076f8eed2a8971c8e234"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -172,6 +172,10 @@
name = "github.com/uber/jaeger-lib"
version = "1.1.0"
[[constraint]]
branch = "v1"
name = "github.com/unrolled/secure"
[[constraint]]
name = "github.com/vdemeester/shakers"
version = "0.1.0"

View file

@ -43,6 +43,7 @@ import (
"github.com/eapache/channels"
"github.com/sirupsen/logrus"
thoas_stats "github.com/thoas/stats"
"github.com/unrolled/secure"
"github.com/urfave/negroni"
"github.com/vulcand/oxy/buffer"
"github.com/vulcand/oxy/connlimit"
@ -936,11 +937,9 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
}
headerMiddleware := middlewares.NewHeaderFromStruct(frontend.Headers)
var responseModifier func(res *http.Response) error
if headerMiddleware != nil {
responseModifier = headerMiddleware.ModifyResponseHeaders
}
secureMiddleware := middlewares.NewSecure(frontend.Headers)
var responseModifier = buildModifyResponse(secureMiddleware, headerMiddleware)
var fwd http.Handler
fwd, err = forward.New(
@ -1136,10 +1135,9 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
n.Use(s.tracingMiddleware.NewNegroniHandlerWrapper("Header", headerMiddleware, false))
}
secureMiddleware := middlewares.NewSecure(frontend.Headers)
if secureMiddleware != nil {
log.Debugf("Adding secure middleware for frontend %s", frontendName)
n.UseFunc(secureMiddleware.HandlerFuncWithNext)
n.UseFunc(secureMiddleware.HandlerFuncWithNextForRequestOnly)
}
if config.Backends[frontend.Backend].Buffering != nil {
@ -1493,3 +1491,21 @@ func (s *Server) buildBufferingMiddleware(handler http.Handler, config *types.Bu
buffer.CondSetter(len(config.RetryExpression) > 0, buffer.Retry(config.RetryExpression)),
)
}
func buildModifyResponse(secure *secure.Secure, header *middlewares.HeaderStruct) func(res *http.Response) error {
return func(res *http.Response) error {
if secure != nil {
err := secure.ModifyResponseHeaders(res)
if err != nil {
return err
}
}
if header != nil {
err := header.ModifyResponseHeaders(res)
if err != nil {
return err
}
}
return nil
}
}

View file

@ -9,6 +9,8 @@ import (
"testing"
"time"
"context"
"github.com/containous/flaeg"
"github.com/containous/mux"
"github.com/containous/traefik/configuration"
@ -22,6 +24,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/unrolled/secure"
"github.com/urfave/negroni"
"github.com/vulcand/oxy/roundrobin"
)
@ -1017,6 +1020,123 @@ func TestBuildRedirectHandler(t *testing.T) {
}
}
type mockContext struct {
headers http.Header
}
func (c mockContext) Deadline() (deadline time.Time, ok bool) {
return deadline, ok
}
func (c mockContext) Done() <-chan struct{} {
ch := make(chan struct{})
close(ch)
return ch
}
func (c mockContext) Err() error {
return context.DeadlineExceeded
}
func (c mockContext) Value(key interface{}) interface{} {
return c.headers
}
func TestNewServerWithResponseModifiers(t *testing.T) {
cases := []struct {
desc string
headerMiddleware *middlewares.HeaderStruct
secureMiddleware *secure.Secure
ctx context.Context
expected map[string]string
}{
{
desc: "header and secure nil",
headerMiddleware: nil,
secureMiddleware: nil,
ctx: mockContext{},
expected: map[string]string{
"X-Default": "powpow",
"Referrer-Policy": "same-origin",
},
},
{
desc: "header middleware not nil",
headerMiddleware: middlewares.NewHeaderFromStruct(&types.Headers{
CustomResponseHeaders: map[string]string{
"X-Default": "powpow",
},
}),
secureMiddleware: nil,
ctx: mockContext{},
expected: map[string]string{
"X-Default": "powpow",
"Referrer-Policy": "same-origin",
},
},
{
desc: "secure middleware not nil",
headerMiddleware: nil,
secureMiddleware: middlewares.NewSecure(&types.Headers{
ReferrerPolicy: "no-referrer",
}),
ctx: mockContext{
headers: http.Header{"Referrer-Policy": []string{"no-referrer"}},
},
expected: map[string]string{
"X-Default": "powpow",
"Referrer-Policy": "no-referrer",
},
},
{
desc: "header and secure middleware not nil",
headerMiddleware: middlewares.NewHeaderFromStruct(&types.Headers{
CustomResponseHeaders: map[string]string{
"Referrer-Policy": "powpow",
},
}),
secureMiddleware: middlewares.NewSecure(&types.Headers{
ReferrerPolicy: "no-referrer",
}),
ctx: mockContext{
headers: http.Header{"Referrer-Policy": []string{"no-referrer"}},
},
expected: map[string]string{
"X-Default": "powpow",
"Referrer-Policy": "powpow",
},
},
}
for _, test := range cases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
headers := make(http.Header)
headers.Add("X-Default", "powpow")
headers.Add("Referrer-Policy", "same-origin")
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
res := &http.Response{
Request: req.WithContext(test.ctx),
Header: headers,
}
responseModifier := buildModifyResponse(test.secureMiddleware, test.headerMiddleware)
err := responseModifier(res)
assert.NoError(t, err)
assert.Equal(t, len(test.expected), len(res.Header))
for k, v := range test.expected {
assert.Equal(t, v, res.Header.Get(k))
}
})
}
}
func buildDynamicConfig(dynamicConfigBuilders ...func(*types.Configuration)) *types.Configuration {
config := &types.Configuration{
Frontends: make(map[string]*types.Frontend),

44
vendor/github.com/unrolled/secure/csp.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
package secure
import (
"context"
"crypto/rand"
"encoding/base64"
"io"
"net/http"
)
type key int
const cspNonceKey key = iota
// CSPNonce returns the nonce value associated with the present request. If no nonce has been generated it returns an empty string.
func CSPNonce(c context.Context) string {
if val, ok := c.Value(cspNonceKey).(string); ok {
return val
}
return ""
}
// WithCSPNonce returns a context derived from ctx containing the given nonce as a value.
//
// This is intended for testing or more advanced use-cases;
// For ordinary HTTP handlers, clients can rely on this package's middleware to populate the CSP nonce in the context.
func WithCSPNonce(ctx context.Context, nonce string) context.Context {
return context.WithValue(ctx, cspNonceKey, nonce)
}
func withCSPNonce(r *http.Request, nonce string) *http.Request {
return r.WithContext(WithCSPNonce(r.Context(), nonce))
}
func cspRandNonce() string {
var buf [cspNonceSize]byte
_, err := io.ReadFull(rand.Reader, buf[:])
if err != nil {
panic("CSP Nonce rand.Reader failed" + err.Error())
}
return base64.RawStdEncoding.EncodeToString(buf[:])
}

View file

@ -1,11 +1,14 @@
package secure
import (
"context"
"fmt"
"net/http"
"strings"
)
type secureCtxKey string
const (
stsHeader = "Strict-Transport-Security"
stsSubdomainString = "; includeSubdomains"
@ -19,8 +22,14 @@ const (
cspHeader = "Content-Security-Policy"
hpkpHeader = "Public-Key-Pins"
referrerPolicyHeader = "Referrer-Policy"
ctxSecureHeaderKey = secureCtxKey("SecureResponseHeader")
cspNonceSize = 16
)
// a type whose pointer is the type of field `SSLHostFunc` of `Options` struct
type SSLHostFunc func(host string) (newHost string)
func defaultBadHostHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Host", http.StatusInternalServerError)
}
@ -37,6 +46,9 @@ type Options struct {
SSLTemporaryRedirect bool
// SSLHost is the host name that is used to redirect http requests to https. Default is "", which indicates to use the same host.
SSLHost string
// SSLHostFunc is a function pointer, the return value of the function is the host name that has same functionality as `SSHost`. Default is nil.
// If SSLHostFunc is nil, the `SSLHost` option will be used.
SSLHostFunc *SSLHostFunc
// SSLProxyHeaders is set of header keys with associated values that would indicate a valid https request. Useful when using Nginx: `map[string]string{"X-Forwarded-Proto": "https"}`. Default is blank map.
SSLProxyHeaders map[string]string
// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.
@ -49,21 +61,27 @@ type Options struct {
ForceSTSHeader bool
// If FrameDeny is set to true, adds the X-Frame-Options header with the value of `DENY`. Default is false.
FrameDeny bool
// CustomFrameOptionsValue allows the X-Frame-Options header value to be set with a custom value. This overrides the FrameDeny option.
// CustomFrameOptionsValue allows the X-Frame-Options header value to be set with a custom value. This overrides the FrameDeny option. Default is "".
CustomFrameOptionsValue string
// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value `nosniff`. Default is false.
ContentTypeNosniff bool
// If BrowserXssFilter is true, adds the X-XSS-Protection header with the value `1; mode=block`. Default is false.
BrowserXssFilter bool
// CustomBrowserXssValue allows the X-XSS-Protection header value to be set with a custom value. This overrides the BrowserXssFilter option. Default is "".
CustomBrowserXssValue string
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
// Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request which can be later retrieved using the Nonce function.
// Eg: script-src $NONCE -> script-src 'nonce-a2ZobGFoZg=='
ContentSecurityPolicy string
// PublicKey implements HPKP to prevent MITM attacks with forged certificates. Default is "".
PublicKey string
// Referrer Policy allows sites to control when browsers will pass the Referer header to other sites. Default is "".
// ReferrerPolicy allows sites to control when browsers will pass the Referer header to other sites. Default is "".
ReferrerPolicy string
// When developing, the AllowedHosts, SSL, and STS options can cause some unwanted effects. Usually testing happens on http, not https, and on localhost, not your production domain... so set this to true for dev environment.
// If you would like your development environment to mimic production with complete Host blocking, SSL redirects, and STS headers, leave this as false. Default if false.
IsDevelopment bool
// nonceEnabled is used internally for dynamic nouces.
nonceEnabled bool
}
// Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be
@ -72,11 +90,11 @@ type Secure struct {
// Customize Secure with an Options struct.
opt Options
// Handlers for when an error occurs (ie bad host).
// badHostHandler is the handler used when an incorrect host is passed in.
badHostHandler http.Handler
}
// New constructs a new Secure instance with supplied options.
// New constructs a new Secure instance with the supplied options.
func New(options ...Options) *Secure {
var o Options
if len(options) == 0 {
@ -85,6 +103,10 @@ func New(options ...Options) *Secure {
o = options[0]
}
o.ContentSecurityPolicy = strings.Replace(o.ContentSecurityPolicy, "$NONCE", "'nonce-%[1]s'", -1)
o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s")
return &Secure{
opt: o,
badHostHandler: http.HandlerFunc(defaultBadHostHandler),
@ -99,6 +121,10 @@ func (s *Secure) SetBadHostHandler(handler http.Handler) {
// Handler implements the http.HandlerFunc for integration with the standard net/http lib.
func (s *Secure) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := s.Process(w, r)
@ -112,8 +138,39 @@ func (s *Secure) Handler(h http.Handler) http.Handler {
})
}
// HandlerForRequestOnly implements the http.HandlerFunc for integration with the standard net/http lib.
// Note that this is for requests only and will not write any headers.
func (s *Secure) HandlerForRequestOnly(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
responseHeader, err := s.processRequest(w, r)
// If there was an error, do not continue.
if err != nil {
return
}
// Save response headers in the request context.
ctx := context.WithValue(r.Context(), ctxSecureHeaderKey, responseHeader)
// No headers will be written to the ResponseWriter.
h.ServeHTTP(w, r.WithContext(ctx))
})
}
// HandlerFuncWithNext is a special implementation for Negroni, but could be used elsewhere.
func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := s.Process(w, r)
// If there was an error, do not call next.
@ -122,8 +179,42 @@ func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, nex
}
}
// Process runs the actual checks and returns an error if the middleware chain should stop.
// HandlerFuncWithNextForRequestOnly is a special implementation for Negroni, but could be used elsewhere.
// Note that this is for requests only and will not write any headers.
func (s *Secure) HandlerFuncWithNextForRequestOnly(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
responseHeader, err := s.processRequest(w, r)
// If there was an error, do not call next.
if err == nil && next != nil {
// Save response headers in the request context
ctx := context.WithValue(r.Context(), ctxSecureHeaderKey, responseHeader)
// No headers will be written to the ResponseWriter.
next(w, r.WithContext(ctx))
}
}
// Process runs the actual checks and writes the headers in the ResponseWriter.
func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {
responseHeader, err := s.processRequest(w, r)
if responseHeader != nil {
for key, values := range responseHeader {
for _, value := range values {
w.Header().Add(key, value)
}
}
}
return err
}
// processRequest runs the actual checks on the request and returns an error if the middleware chain should stop.
func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.Header, error) {
// Resolve the host for the request, using proxy headers if present.
host := r.Host
for _, header := range s.opt.HostsProxyHeaders {
@ -145,28 +236,24 @@ func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {
if !isGoodHost {
s.badHostHandler.ServeHTTP(w, r)
return fmt.Errorf("Bad host name: %s", host)
return nil, fmt.Errorf("bad host name: %s", host)
}
}
// Determine if we are on HTTPS.
isSSL := strings.EqualFold(r.URL.Scheme, "https") || r.TLS != nil
if !isSSL {
for k, v := range s.opt.SSLProxyHeaders {
if r.Header.Get(k) == v {
isSSL = true
break
}
}
}
ssl := s.isSSL(r)
// SSL check.
if s.opt.SSLRedirect && !isSSL && !s.opt.IsDevelopment {
if s.opt.SSLRedirect && !ssl && !s.opt.IsDevelopment {
url := r.URL
url.Scheme = "https"
url.Host = host
if len(s.opt.SSLHost) > 0 {
if s.opt.SSLHostFunc != nil {
if h := (*s.opt.SSLHostFunc)(host); len(h) > 0 {
url.Host = h
}
} else if len(s.opt.SSLHost) > 0 {
url.Host = s.opt.SSLHost
}
@ -176,12 +263,15 @@ func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {
}
http.Redirect(w, r, url.String(), status)
return fmt.Errorf("Redirecting to HTTPS")
return nil, fmt.Errorf("redirecting to HTTPS")
}
// Create our header container.
responseHeader := make(http.Header)
// Strict Transport Security header. Only add header when we know it's an SSL connection.
// See https://tools.ietf.org/html/rfc6797#section-7.2 for details.
if s.opt.STSSeconds != 0 && (isSSL || s.opt.ForceSTSHeader) && !s.opt.IsDevelopment {
if s.opt.STSSeconds != 0 && (ssl || s.opt.ForceSTSHeader) && !s.opt.IsDevelopment {
stsSub := ""
if s.opt.STSIncludeSubdomains {
stsSub = stsSubdomainString
@ -191,40 +281,76 @@ func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {
stsSub += stsPreloadString
}
w.Header().Add(stsHeader, fmt.Sprintf("max-age=%d%s", s.opt.STSSeconds, stsSub))
responseHeader.Set(stsHeader, fmt.Sprintf("max-age=%d%s", s.opt.STSSeconds, stsSub))
}
// Frame Options header.
if len(s.opt.CustomFrameOptionsValue) > 0 {
w.Header().Add(frameOptionsHeader, s.opt.CustomFrameOptionsValue)
responseHeader.Set(frameOptionsHeader, s.opt.CustomFrameOptionsValue)
} else if s.opt.FrameDeny {
w.Header().Add(frameOptionsHeader, frameOptionsValue)
responseHeader.Set(frameOptionsHeader, frameOptionsValue)
}
// Content Type Options header.
if s.opt.ContentTypeNosniff {
w.Header().Add(contentTypeHeader, contentTypeValue)
responseHeader.Set(contentTypeHeader, contentTypeValue)
}
// XSS Protection header.
if s.opt.BrowserXssFilter {
w.Header().Add(xssProtectionHeader, xssProtectionValue)
if len(s.opt.CustomBrowserXssValue) > 0 {
responseHeader.Set(xssProtectionHeader, s.opt.CustomBrowserXssValue)
} else if s.opt.BrowserXssFilter {
responseHeader.Set(xssProtectionHeader, xssProtectionValue)
}
// HPKP header.
if len(s.opt.PublicKey) > 0 && isSSL && !s.opt.IsDevelopment {
w.Header().Add(hpkpHeader, s.opt.PublicKey)
if len(s.opt.PublicKey) > 0 && ssl && !s.opt.IsDevelopment {
responseHeader.Set(hpkpHeader, s.opt.PublicKey)
}
// Content Security Policy header.
if len(s.opt.ContentSecurityPolicy) > 0 {
w.Header().Add(cspHeader, s.opt.ContentSecurityPolicy)
if s.opt.nonceEnabled {
responseHeader.Set(cspHeader, fmt.Sprintf(s.opt.ContentSecurityPolicy, CSPNonce(r.Context())))
} else {
responseHeader.Set(cspHeader, s.opt.ContentSecurityPolicy)
}
}
// Referrer Policy header.
if len(s.opt.ReferrerPolicy) > 0 {
w.Header().Add(referrerPolicyHeader, s.opt.ReferrerPolicy)
responseHeader.Set(referrerPolicyHeader, s.opt.ReferrerPolicy)
}
return responseHeader, nil
}
// isSSL determine if we are on HTTPS.
func (s *Secure) isSSL(r *http.Request) bool {
ssl := strings.EqualFold(r.URL.Scheme, "https") || r.TLS != nil
if !ssl {
for k, v := range s.opt.SSLProxyHeaders {
if r.Header.Get(k) == v {
ssl = true
break
}
}
}
return ssl
}
// ModifyResponseHeaders modifies the Response.
// Used by http.ReverseProxy.
func (s *Secure) ModifyResponseHeaders(res *http.Response) error {
if res != nil && res.Request != nil {
responseHeader := res.Request.Context().Value(ctxSecureHeaderKey)
if responseHeader != nil {
for header, values := range responseHeader.(http.Header) {
if len(values) > 0 {
res.Header.Set(header, strings.Join(values, ","))
}
}
}
}
return nil
}