From c88f91938057576c002f31c1f4c32802f035fac5 Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Tue, 22 Oct 2024 17:38:51 -0400 Subject: [PATCH] feat: add coep, corp, x-dns-prefetch-control, x-permitted-cross-doman-policies (#102) * feat: add coep, corp, x-dns-prefetch-control, x-permitted-cross-domain-policies headers * updated readme * update XDNSPrefetchControl to be of type string and fix test * add newly added variables in the default section * remove len check --- README.md | 9 +++++- secure.go | 76 +++++++++++++++++++++++++++++++++++++------------- secure_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6bdb95c..cf8b92d 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,10 @@ s := secure.New(secure.Options{ FeaturePolicy: "vibrate 'none';", // Deprecated: this header has been renamed to PermissionsPolicy. FeaturePolicy allows the Feature-Policy header with the value to be set with a custom value. Default is "". PermissionsPolicy: "fullscreen=(), geolocation=()", // PermissionsPolicy allows the Permissions-Policy header with the value to be set with a custom value. Default is "". CrossOriginOpenerPolicy: "same-origin", // CrossOriginOpenerPolicy allows the Cross-Origin-Opener-Policy header with the value to be set with a custom value. Default is "". - + CrossOriginEmbedderPolicy: "require-corp", // CrossOriginEmbedderPolicy allows the Cross-Origin-Embedder-Policy header with the value to be set with a custom value. Default is "". + CrossOriginResourcePolicy: "same-origin", // CrossOriginResourcePolicy allows the Cross-Origin-Resource-Policy header with the value to be set with a custom value. Default is "". + XDNSPrefetchControl: "on", // XDNSPrefetchControl allows the X-DNS-Prefetch-Control header to be set via "on" or "off" keyword. Default is "". + XPermittedCrossDomainPolicies: "none", // XPermittedCrossDomainPolicies allows the X-Permitted-Cross-Domain-Policies to be set with a custom value. Default is "". IsDevelopment: true, // This will cause the AllowedHosts, SSLRedirect, and STSSeconds/STSIncludeSubdomains options to be ignored during development. When deploying to production, be sure to set this to false. }) // ... @@ -121,6 +124,10 @@ l := secure.New(secure.Options{ FeaturePolicy: "", PermissionsPolicy: "", CrossOriginOpenerPolicy: "", + CrossOriginEmbedderPolicy: "", + CrossOriginResourcePolicy: "", + XDNSPrefetchControl: "", + XPermittedCrossDomainPolicies: "", IsDevelopment: false, }) ~~~ diff --git a/secure.go b/secure.go index 9b93960..122c43c 100644 --- a/secure.go +++ b/secure.go @@ -11,25 +11,28 @@ import ( type secureCtxKey string const ( - stsHeader = "Strict-Transport-Security" - stsSubdomainString = "; includeSubDomains" - stsPreloadString = "; preload" - frameOptionsHeader = "X-Frame-Options" - frameOptionsValue = "DENY" - contentTypeHeader = "X-Content-Type-Options" - contentTypeValue = "nosniff" - 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" - permissionsPolicyHeader = "Permissions-Policy" - coopHeader = "Cross-Origin-Opener-Policy" - - ctxDefaultSecureHeaderKey = secureCtxKey("SecureResponseHeader") - cspNonceSize = 16 + stsHeader = "Strict-Transport-Security" + stsSubdomainString = "; includeSubDomains" + stsPreloadString = "; preload" + frameOptionsHeader = "X-Frame-Options" + frameOptionsValue = "DENY" + contentTypeHeader = "X-Content-Type-Options" + contentTypeValue = "nosniff" + 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" + permissionsPolicyHeader = "Permissions-Policy" + coopHeader = "Cross-Origin-Opener-Policy" + coepHeader = "Cross-Origin-Embedder-Policy" + corpHeader = "Cross-Origin-Resource-Policy" + dnsPreFetchControlHeader = "X-DNS-Prefetch-Control" + permittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" + ctxDefaultSecureHeaderKey = secureCtxKey("SecureResponseHeader") + cspNonceSize = 16 ) // SSLHostFunc is a custom function type that can be used to dynamically set the SSL host of a request. @@ -91,6 +94,18 @@ type Options struct { // CrossOriginOpenerPolicy allows you to ensure a top-level document does not share a browsing context group with cross-origin documents. Default is "". // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy CrossOriginOpenerPolicy string + // CrossOriginResourcePolicy header blocks others from loading your resources cross-origin in some cases. + // Reference https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy + CrossOriginResourcePolicy string + // CrossOriginEmbedderPolicy header helps control what resources can be loaded cross-origin. + // Reference https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy + CrossOriginEmbedderPolicy string + // XDNSPrefetchControl header helps control DNS prefetching, which can improve user privacy at the expense of performance. + // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control + XDNSPrefetchControl string + // XPermittedCrossDomainPolicies header tells some clients (mostly Adobe products) your domain's policy for loading cross-domain content. + // Reference: https://owasp.org/www-project-secure-headers/ + XPermittedCrossDomainPolicies 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 slice of fully qualified domain names that are allowed. Default is an empty slice, which allows any and all host names. @@ -466,6 +481,29 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He responseHeader.Set(coopHeader, s.opt.CrossOriginOpenerPolicy) } + // Cross Origin Resource Policy header. + if len(s.opt.CrossOriginResourcePolicy) > 0 { + responseHeader.Set(corpHeader, s.opt.CrossOriginResourcePolicy) + } + + // Cross-Origin-Embedder-Policy header. + if len(s.opt.CrossOriginEmbedderPolicy) > 0 { + responseHeader.Set(coepHeader, s.opt.CrossOriginEmbedderPolicy) + } + + // X-DNS-Prefetch-Control header. + switch strings.ToLower(s.opt.XDNSPrefetchControl) { + case "on": + responseHeader.Set(dnsPreFetchControlHeader, "on") + case "off": + responseHeader.Set(dnsPreFetchControlHeader, "off") + } + + // X-Permitted-Cross-Domain-Policies header. + if len(s.opt.XPermittedCrossDomainPolicies) > 0 { + responseHeader.Set(permittedCrossDomainPolicies, s.opt.XPermittedCrossDomainPolicies) + } + return responseHeader, r, nil } diff --git a/secure_test.go b/secure_test.go index 80d8d2a..52a5c63 100644 --- a/secure_test.go +++ b/secure_test.go @@ -1046,6 +1046,74 @@ func TestCrossOriginOpenerPolicy(t *testing.T) { expect(t, res.Header().Get("Cross-Origin-Opener-Policy"), "same-origin") } +func TestCrossOriginEmbedderPolicy(t *testing.T) { + s := New(Options{ + CrossOriginEmbedderPolicy: "require-corp", + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/foo", nil) + + s.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("Cross-Origin-Embedder-Policy"), "require-corp") +} + +func TestCrossOriginResourcePolicy(t *testing.T) { + s := New(Options{ + CrossOriginResourcePolicy: "same-origin", + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/foo", nil) + + s.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("Cross-Origin-Resource-Policy"), "same-origin") +} + +func TestXDNSPreFetchControl(t *testing.T) { + s := New(Options{ + XDNSPrefetchControl: "on", + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/foo", nil) + + s.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("X-DNS-Prefetch-Control"), "on") + + k := New(Options{ + XDNSPrefetchControl: "off", + }) + + res = httptest.NewRecorder() + req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/bar", nil) + + k.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("X-DNS-Prefetch-Control"), "off") +} + +func TestXPermittedCrossDomainPolicies(t *testing.T) { + s := New(Options{ + XPermittedCrossDomainPolicies: "none", + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/foo", nil) + + s.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("X-Permitted-Cross-Domain-Policies"), "none") +} + func TestIsSSL(t *testing.T) { s := New(Options{ SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},