From a3c6a3a34f53b025097a576907343de58b611166 Mon Sep 17 00:00:00 2001 From: Patrick O'Doherty Date: Thu, 3 Oct 2024 19:38:29 +0100 Subject: [PATCH] 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 --- safeweb/http.go | 13 ++++++++++++ safeweb/http_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/safeweb/http.go b/safeweb/http.go index d04a84bb5..9130b42d3 100644 --- a/safeweb/http.go +++ b/safeweb/http.go @@ -94,6 +94,10 @@ `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. type Config struct { // 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 // default is to set SameSite=Strict. 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 { @@ -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("X-Content-Type-Options", "nosniff") 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) } diff --git a/safeweb/http_test.go b/safeweb/http_test.go index f48aa64a7..843da08aa 100644 --- a/safeweb/http_test.go +++ b/safeweb/http_test.go @@ -11,6 +11,7 @@ "strings" "testing" + "github.com/google/go-cmp/cmp" "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")) + } + }) + } +}