Fix Location response header http to https when SSL

This commit is contained in:
Eliel Goncalves
2019-10-07 05:16:05 -03:00
committed by Traefiker Bot
parent 74ad83f05a
commit 590a0d67bb
2 changed files with 118 additions and 43 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
)
@@ -11,7 +12,7 @@ type secureCtxKey string
const (
stsHeader = "Strict-Transport-Security"
stsSubdomainString = "; includeSubdomains"
stsSubdomainString = "; includeSubDomains"
stsPreloadString = "; preload"
frameOptionsHeader = "X-Frame-Options"
frameOptionsValue = "DENY"
@@ -20,8 +21,11 @@ const (
xssProtectionHeader = "X-XSS-Protection"
xssProtectionValue = "1; mode=block"
cspHeader = "Content-Security-Policy"
cspReportOnlyHeader = "Content-Security-Policy-Report-Only"
hpkpHeader = "Public-Key-Pins"
referrerPolicyHeader = "Referrer-Policy"
featurePolicyHeader = "Feature-Policy"
expectCTHeader = "Expect-CT"
ctxSecureHeaderKey = secureCtxKey("SecureResponseHeader")
cspNonceSize = 16
@@ -61,6 +65,8 @@ type Options struct {
STSPreload bool
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
ContentSecurityPolicy string
// ContentSecurityPolicyReportOnly allows the Content-Security-Policy-Report-Only header value to be set with a custom value. Default is "".
ContentSecurityPolicyReportOnly string
// CustomBrowserXssValue allows the X-XSS-Protection header value to be set with a custom value. This overrides the BrowserXssFilter option. Default is "".
CustomBrowserXssValue string // nolint: golint
// 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.
@@ -71,10 +77,15 @@ type Options struct {
PublicKey string
// ReferrerPolicy allows sites to control when browsers will pass the Referer header to other sites. Default is "".
ReferrerPolicy string
// FeaturePolicy allows to selectively enable and disable use of various browser features and APIs. Default is "".
FeaturePolicy string
// 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
// AllowedHosts is a list of fully qualified domain names that are allowed. Default is empty list, which allows any and all host names.
AllowedHosts []string
// AllowedHostsAreRegex determines, if the provided slice contains valid regular expressions. If this flag is set to true, every request's
// host will be checked against these expressions. Default is false for backwards compatibility.
AllowedHostsAreRegex bool
// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request.
HostsProxyHeaders []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.
@@ -84,6 +95,8 @@ type Options struct {
SSLProxyHeaders map[string]string
// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.
STSSeconds int64
// ExpectCTHeader allows the Expect-CT header value to be set with a custom value. Default is "".
ExpectCTHeader string
}
// Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be
@@ -94,6 +107,10 @@ type Secure struct {
// badHostHandler is the handler used when an incorrect host is passed in.
badHostHandler http.Handler
// cRegexAllowedHosts saves the compiled regular expressions of the AllowedHosts
// option for subsequent use in processRequest
cRegexAllowedHosts []*regexp.Regexp
}
// New constructs a new Secure instance with the supplied options.
@@ -106,13 +123,27 @@ func New(options ...Options) *Secure {
}
o.ContentSecurityPolicy = strings.Replace(o.ContentSecurityPolicy, "$NONCE", "'nonce-%[1]s'", -1)
o.ContentSecurityPolicyReportOnly = strings.Replace(o.ContentSecurityPolicyReportOnly, "$NONCE", "'nonce-%[1]s'", -1)
o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s")
o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s") || strings.Contains(o.ContentSecurityPolicyReportOnly, "%[1]s")
return &Secure{
s := &Secure{
opt: o,
badHostHandler: http.HandlerFunc(defaultBadHostHandler),
}
if s.opt.AllowedHostsAreRegex {
// Test for invalid regular expressions in AllowedHosts
for _, allowedHost := range o.AllowedHosts {
regex, err := regexp.Compile(fmt.Sprintf("^%s$", allowedHost))
if err != nil {
panic(fmt.Sprintf("Error parsing AllowedHost: %s", err))
}
s.cRegexAllowedHosts = append(s.cRegexAllowedHosts, regex)
}
}
return s
}
// SetBadHostHandler sets the handler to call when secure rejects the host name.
@@ -123,13 +154,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)
responseHeader, r, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
// If there was an error, do not continue.
if err != nil {
@@ -144,13 +172,9 @@ func (s *Secure) Handler(h http.Handler) http.Handler {
// 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)
responseHeader, r, err := s.processRequest(w, r)
// If there was an error, do not continue.
if err != nil {
@@ -167,13 +191,10 @@ func (s *Secure) HandlerForRequestOnly(h http.Handler) http.Handler {
// 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)
responseHeader, r, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
// If there was an error, do not call next.
if err == nil && next != nil {
@@ -184,13 +205,9 @@ func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, nex
// 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)
responseHeader, r, err := s.processRequest(w, r)
// If there was an error, do not call next.
if err == nil && next != nil {
@@ -202,21 +219,44 @@ func (s *Secure) HandlerFuncWithNextForRequestOnly(w http.ResponseWriter, r *htt
}
}
// 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)
}
// addResponseHeaders Adds the headers from 'responseHeader' to the response.
func addResponseHeaders(responseHeader http.Header, w http.ResponseWriter) {
for key, values := range responseHeader {
for _, value := range values {
w.Header().Set(key, value)
}
}
}
// 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)
addResponseHeaders(responseHeader, w)
return err
}
// ProcessAndReturnNonce runs the actual checks and writes the headers in the ResponseWriter.
// In addition, the generated nonce for the request is returned as well as the error value.
func (s *Secure) ProcessAndReturnNonce(w http.ResponseWriter, r *http.Request) (string, error) {
responseHeader, newR, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
return CSPNonce(newR.Context()), err
}
// ProcessNoModifyRequest runs the actual checks but does not write the headers in the ResponseWriter.
func (s *Secure) ProcessNoModifyRequest(w http.ResponseWriter, r *http.Request) (http.Header, *http.Request, error) {
return s.processRequest(w, r)
}
// 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) {
func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.Header, *http.Request, error) {
// Setup nonce if required.
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Resolve the host for the request, using proxy headers if present.
host := r.Host
for _, header := range s.opt.HostsProxyHeaders {
@@ -229,16 +269,25 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
// Allowed hosts check.
if len(s.opt.AllowedHosts) > 0 && !s.opt.IsDevelopment {
isGoodHost := false
for _, allowedHost := range s.opt.AllowedHosts {
if strings.EqualFold(allowedHost, host) {
isGoodHost = true
break
if s.opt.AllowedHostsAreRegex {
for _, allowedHost := range s.cRegexAllowedHosts {
if match := allowedHost.MatchString(host); match {
isGoodHost = true
break
}
}
} else {
for _, allowedHost := range s.opt.AllowedHosts {
if strings.EqualFold(allowedHost, host) {
isGoodHost = true
break
}
}
}
if !isGoodHost {
s.badHostHandler.ServeHTTP(w, r)
return nil, fmt.Errorf("bad host name: %s", host)
return nil, nil, fmt.Errorf("bad host name: %s", host)
}
}
@@ -265,11 +314,11 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
}
http.Redirect(w, r, url.String(), status)
return nil, fmt.Errorf("redirecting to HTTPS")
return nil, nil, fmt.Errorf("redirecting to HTTPS")
}
if s.opt.SSLForceHost {
var SSLHost = host;
var SSLHost = host
if s.opt.SSLHostFunc != nil {
if h := (*s.opt.SSLHostFunc)(host); len(h) > 0 {
SSLHost = h
@@ -288,7 +337,7 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
}
http.Redirect(w, r, url.String(), status)
return nil, fmt.Errorf("redirecting to HTTPS")
return nil, nil, fmt.Errorf("redirecting to HTTPS")
}
}
@@ -343,12 +392,31 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
}
}
// Content Security Policy Report Only header.
if len(s.opt.ContentSecurityPolicyReportOnly) > 0 {
if s.opt.nonceEnabled {
responseHeader.Set(cspReportOnlyHeader, fmt.Sprintf(s.opt.ContentSecurityPolicyReportOnly, CSPNonce(r.Context())))
} else {
responseHeader.Set(cspReportOnlyHeader, s.opt.ContentSecurityPolicyReportOnly)
}
}
// Referrer Policy header.
if len(s.opt.ReferrerPolicy) > 0 {
responseHeader.Set(referrerPolicyHeader, s.opt.ReferrerPolicy)
}
return responseHeader, nil
// Feature Policy header.
if len(s.opt.FeaturePolicy) > 0 {
responseHeader.Set(featurePolicyHeader, s.opt.FeaturePolicy)
}
// Expect-CT header.
if len(s.opt.ExpectCTHeader) > 0 {
responseHeader.Set(expectCTHeader, s.opt.ExpectCTHeader)
}
return responseHeader, r, nil
}
// isSSL determine if we are on HTTPS.
@@ -369,6 +437,13 @@ func (s *Secure) isSSL(r *http.Request) bool {
// Used by http.ReverseProxy.
func (s *Secure) ModifyResponseHeaders(res *http.Response) error {
if res != nil && res.Request != nil {
// Fix Location response header http to https when SSL is enabled.
location := res.Header.Get("Location")
if s.isSSL(res.Request) && strings.Contains(location, "http:") {
location = strings.Replace(location, "http:", "https:", 1)
res.Header.Set("Location", location)
}
responseHeader := res.Request.Context().Value(ctxSecureHeaderKey)
if responseHeader != nil {
for header, values := range responseHeader.(http.Header) {