diff --git a/internal/httputil/securityheader/securityheader.go b/internal/httputil/securityheader/securityheader.go index 47fd8d26..42cf1e2f 100644 --- a/internal/httputil/securityheader/securityheader.go +++ b/internal/httputil/securityheader/securityheader.go @@ -9,12 +9,22 @@ import "net/http" // Wrap the provided http.Handler so it sets appropriate security-related response headers. func Wrap(wrapped http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + wrapped.ServeHTTP(w, r) h := w.Header() h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") h.Set("X-Frame-Options", "DENY") h.Set("X-XSS-Protection", "1; mode=block") h.Set("X-Content-Type-Options", "nosniff") h.Set("Referrer-Policy", "no-referrer") - wrapped.ServeHTTP(w, r) + h.Set("X-DNS-Prefetch-Control", "off") + + // first overwrite existing Cache-Control header with Set, then append more headers with Add + h.Set("Cache-Control", "no-cache") + h.Add("Cache-Control", "no-store") + h.Add("Cache-Control", "max-age=0") + h.Add("Cache-Control", "must-revalidate") + + h.Set("Pragma", "no-cache") + h.Set("Expires", "0") }) } diff --git a/internal/httputil/securityheader/securityheader_test.go b/internal/httputil/securityheader/securityheader_test.go index e5527cb6..715e7cd5 100644 --- a/internal/httputil/securityheader/securityheader_test.go +++ b/internal/httputil/securityheader/securityheader_test.go @@ -26,5 +26,9 @@ func TestWrap(t *testing.T) { "X-Content-Type-Options": []string{"nosniff"}, "X-Frame-Options": []string{"DENY"}, "X-Xss-Protection": []string{"1; mode=block"}, + "X-Dns-Prefetch-Control": []string{"off"}, + "Cache-Control": []string{"no-cache", "no-store", "max-age=0", "must-revalidate"}, + "Pragma": []string{"no-cache"}, + "Expires": []string{"0"}, }, rec.Header()) } diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 5bfe7947..774cff26 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -16,6 +16,7 @@ import ( "golang.org/x/oauth2" "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/provider" @@ -34,7 +35,7 @@ func NewHandler( upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, ) http.Handler { - return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return securityheader.Wrap(httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { if r.Method != http.MethodPost && r.Method != http.MethodGet { // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Authorization Servers MUST support the use of the HTTP GET and POST methods defined in @@ -142,7 +143,7 @@ func NewHandler( ) return nil - }) + })) } func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index ee07552b..e0f7eaea 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -773,6 +773,7 @@ func TestAuthorizationEndpoint(t *testing.T) { require.Equal(t, test.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType) + testutil.RequireSecurityHeaders(t, rsp) actualLocation := rsp.Header().Get("Location") if test.wantLocationHeader != "" { diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index 2cea6c2e..c331e653 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -17,6 +17,7 @@ import ( "github.com/ory/fosite/token/jwt" "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/provider" @@ -45,7 +46,7 @@ func NewHandler( stateDecoder, cookieDecoder oidc.Decoder, redirectURI string, ) http.Handler { - return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return securityheader.Wrap(httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { state, err := validateRequest(r, stateDecoder, cookieDecoder) if err != nil { return err @@ -108,7 +109,7 @@ func NewHandler( oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder) return nil - }) + })) } func authcode(r *http.Request) string { diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 38d12b29..752e07d2 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -477,6 +477,8 @@ func TestCallbackEndpoint(t *testing.T) { t.Logf("response: %#v", rsp) t.Logf("response body: %q", rsp.Body.String()) + testutil.RequireSecurityHeaders(t, rsp) + if test.wantExchangeAndValidateTokensCall != nil { require.Equal(t, 1, test.idp.ExchangeAuthcodeAndValidateTokensCallCount()) test.wantExchangeAndValidateTokensCall.Ctx = req.Context() diff --git a/internal/testutil/assertions.go b/internal/testutil/assertions.go index 53f5f73a..b0c3018d 100644 --- a/internal/testutil/assertions.go +++ b/internal/testutil/assertions.go @@ -6,6 +6,7 @@ package testutil import ( "context" "mime" + "net/http/httptest" "testing" "time" @@ -52,3 +53,15 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret require.NoError(t, err) require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets) } + +func RequireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) { + require.Equal(t, "default-src 'none'; frame-ancestors 'none'", response.Header().Get("Content-Security-Policy")) + require.Equal(t, "DENY", response.Header().Get("X-Frame-Options")) + require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection")) + require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options")) + require.Equal(t, "no-referrer", response.Header().Get("Referrer-Policy")) + require.Equal(t, "off", response.Header().Get("X-DNS-Prefetch-Control")) + require.ElementsMatch(t, []string{"no-cache", "no-store", "max-age=0", "must-revalidate"}, response.Header().Values("Cache-Control")) + require.Equal(t, "no-cache", response.Header().Get("Pragma")) + require.Equal(t, "0", response.Header().Get("Expires")) +}