safeweb: add StrictTransportSecurityOptions config (#13679)

Add the ability to specify Strict-Transport-Security options in response
to BrowserMux HTTP requests in safeweb.

Updates https://github.com/tailscale/corp/issues/23375

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
angott/doh-clients-sleep-mode
Patrick O'Doherty 3 weeks ago committed by GitHub
parent dc60c8d786
commit a3c6a3a34f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -94,6 +94,10 @@ var defaultCSP = strings.Join([]string{
`object-src 'self'`, // disallow embedding of resources from other origins `object-src 'self'`, // disallow embedding of resources from other origins
}, "; ") }, "; ")
// The default Strict-Transport-Security header. This header tells the browser
// to exclusively use HTTPS for all requests to the origin for the next year.
var DefaultStrictTransportSecurityOptions = "max-age=31536000"
// Config contains the configuration for a safeweb server. // Config contains the configuration for a safeweb server.
type Config struct { type Config struct {
// SecureContext specifies whether the Server is running in a secure (HTTPS) context. // SecureContext specifies whether the Server is running in a secure (HTTPS) context.
@ -134,6 +138,12 @@ type Config struct {
// CookiesSameSiteLax specifies whether to use SameSite=Lax in cookies. The // CookiesSameSiteLax specifies whether to use SameSite=Lax in cookies. The
// default is to set SameSite=Strict. // default is to set SameSite=Strict.
CookiesSameSiteLax bool CookiesSameSiteLax bool
// StrictTransportSecurityOptions specifies optional directives for the
// Strict-Transport-Security header sent in response to requests made to the
// BrowserMux when SecureContext is true.
// If empty, it defaults to max-age of 1 year.
StrictTransportSecurityOptions string
} }
func (c *Config) setDefaults() error { func (c *Config) setDefaults() error {
@ -274,6 +284,9 @@ func (s *Server) serveBrowser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", s.csp) w.Header().Set("Content-Security-Policy", s.csp)
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referer-Policy", "same-origin") w.Header().Set("Referer-Policy", "same-origin")
if s.SecureContext {
w.Header().Set("Strict-Transport-Security", cmp.Or(s.StrictTransportSecurityOptions, DefaultStrictTransportSecurityOptions))
}
s.csrfProtect(s.BrowserMux).ServeHTTP(w, r) s.csrfProtect(s.BrowserMux).ServeHTTP(w, r)
} }

@ -11,6 +11,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
@ -561,3 +562,50 @@ func TestGetMoreSpecificPattern(t *testing.T) {
}) })
} }
} }
func TestStrictTransportSecurityOptions(t *testing.T) {
tests := []struct {
name string
options string
secureContext bool
expect string
}{
{
name: "off by default",
},
{
name: "default HSTS options in the secure context",
secureContext: true,
expect: DefaultStrictTransportSecurityOptions,
},
{
name: "custom options sent in the secure context",
options: DefaultStrictTransportSecurityOptions + "; includeSubDomains",
secureContext: true,
expect: DefaultStrictTransportSecurityOptions + "; includeSubDomains",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &http.ServeMux{}
h.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
s, err := NewServer(Config{BrowserMux: h, SecureContext: tt.secureContext, StrictTransportSecurityOptions: tt.options})
if err != nil {
t.Fatal(err)
}
defer s.Close()
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
s.h.Handler.ServeHTTP(w, req)
resp := w.Result()
if cmp.Diff(tt.expect, resp.Header.Get("Strict-Transport-Security")) != "" {
t.Fatalf("HSTS want: %q; got: %q", tt.expect, resp.Header.Get("Strict-Transport-Security"))
}
})
}
}

Loading…
Cancel
Save