From 9fb3cb294928c1b6760eb7294b14ba799343d9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:47:15 +0200 Subject: [PATCH 1/4] chore: enable dependabot --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b4c78be --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: monthly From c0cf2ed871df3adb49606a78e24f242c1e22efc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:29:56 +0200 Subject: [PATCH 2/4] fix: lint --- cspbuilder/builder_test.go | 8 +++++--- cspbuilder/directive_builder_test.go | 2 ++ secure.go | 9 +++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cspbuilder/builder_test.go b/cspbuilder/builder_test.go index c0c4f16..c74ad98 100644 --- a/cspbuilder/builder_test.go +++ b/cspbuilder/builder_test.go @@ -44,6 +44,7 @@ func TestContentSecurityPolicyBuilder_Build_SingleDirective(t *testing.T) { } got, err := builder.Build() + if (err != nil) != tt.wantErr { t.Errorf("ContentSecurityPolicyBuilder.Build() error = %v, wantErr %v", err, tt.wantErr) @@ -60,7 +61,7 @@ func TestContentSecurityPolicyBuilder_Build_SingleDirective(t *testing.T) { func TestContentSecurityPolicyBuilder_Build_MultipleDirectives(t *testing.T) { tests := []struct { name string - directives map[string]([]string) + directives map[string][]string builder Builder wantParts []string wantFull string @@ -68,7 +69,7 @@ func TestContentSecurityPolicyBuilder_Build_MultipleDirectives(t *testing.T) { }{ { name: "multiple valid directives", - directives: map[string]([]string){ + directives: map[string][]string{ "default-src": {"'self'", "example.com", "*.example.com"}, "sandbox": {"allow-scripts"}, "frame-ancestors": {"'self'", "http://*.example.com"}, @@ -98,6 +99,7 @@ func TestContentSecurityPolicyBuilder_Build_MultipleDirectives(t *testing.T) { } got, err := builder.Build() + if (err != nil) != tt.wantErr { t.Errorf("ContentSecurityPolicyBuilder.Build() error = %v, wantErr %v", err, tt.wantErr) @@ -125,7 +127,7 @@ func TestContentSecurityPolicyBuilder_Build_MultipleDirectives(t *testing.T) { } if strings.HasSuffix(got, " ") { - t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', ends on whitespace", got) + t.Errorf("ContentSecurityPolicyBuilder.Bui ld() = '%v', ends on whitespace", got) } if strings.HasSuffix(got, ";") { diff --git a/cspbuilder/directive_builder_test.go b/cspbuilder/directive_builder_test.go index 8cab58d..e863bb4 100644 --- a/cspbuilder/directive_builder_test.go +++ b/cspbuilder/directive_builder_test.go @@ -81,6 +81,7 @@ func TestBuildDirectiveFrameAncestors(t *testing.T) { sb := &strings.Builder{} err := buildDirectiveFrameAncestors(sb, tt.values) + if tt.wantErr && err != nil { return } @@ -225,6 +226,7 @@ func TestBuildDirectiveTrustedTypes(t *testing.T) { sb := &strings.Builder{} err := buildDirectiveTrustedTypes(sb, tt.values) + if tt.wantErr && err != nil { return } diff --git a/secure.go b/secure.go index 122c43c..0ccbd8a 100644 --- a/secure.go +++ b/secure.go @@ -2,6 +2,7 @@ package secure import ( "context" + "errors" "fmt" "net/http" "regexp" @@ -369,7 +370,7 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He http.Redirect(w, r, url.String(), status) - return nil, nil, fmt.Errorf("redirecting to HTTPS") + return nil, nil, errors.New("redirecting to HTTPS") } if s.opt.SSLForceHost { @@ -395,7 +396,7 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He http.Redirect(w, r, url.String(), status) - return nil, nil, fmt.Errorf("redirecting to HTTPS") + return nil, nil, errors.New("redirecting to HTTPS") } } @@ -403,7 +404,7 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He if s.opt.AllowRequestFunc != nil && !s.opt.AllowRequestFunc(r) { s.badRequestHandler.ServeHTTP(w, r) - return nil, nil, fmt.Errorf("request not allowed") + return nil, nil, errors.New("request not allowed") } // Create our header container. @@ -535,7 +536,7 @@ func (s *Secure) ModifyResponseHeaders(res *http.Response) error { location := res.Header.Get("Location") if s.isSSL(res.Request) && len(s.opt.SSLHost) > 0 && - (strings.HasPrefix(location, fmt.Sprintf("http://%s/", s.opt.SSLHost)) || location == fmt.Sprintf("http://%s", s.opt.SSLHost)) { + (strings.HasPrefix(location, fmt.Sprintf("http://%s/", s.opt.SSLHost)) || location == "http://"+s.opt.SSLHost) { location = strings.Replace(location, "http:", "https:", 1) res.Header.Set("Location", location) } From 01f60c6caca28e9e700d3067ad2d3728754aea2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:03:57 +0200 Subject: [PATCH 3/4] feat: add support for `X-Robots-Tag` and `X-Permitted-Cross-Domain-Policies` --- README.md | 34 ++++++++++++++++++---------------- secure.go | 21 ++++++++++++++++++++- secure_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cf8b92d..e7e80e2 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,21 @@ var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { func main() { secureMiddleware := secure.New(secure.Options{ - AllowedHosts: []string{"example\\.com", ".*\\.example\\.com"}, - AllowedHostsAreRegex: true, - HostsProxyHeaders: []string{"X-Forwarded-Host"}, - SSLRedirect: true, - SSLHost: "ssl.example.com", - SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, - STSSeconds: 31536000, - STSIncludeSubdomains: true, - STSPreload: true, - FrameDeny: true, - ContentTypeNosniff: true, - BrowserXssFilter: true, - ContentSecurityPolicy: "script-src $NONCE", + AllowedHosts: []string{"example\\.com", ".*\\.example\\.com"}, + AllowedHostsAreRegex: true, + HostsProxyHeaders: []string{"X-Forwarded-Host"}, + SSLRedirect: true, + SSLHost: "ssl.example.com", + SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, + STSSeconds: 31536000, + STSIncludeSubdomains: true, + STSPreload: true, + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXssFilter: true, + ContentSecurityPolicy: "script-src $NONCE", + PermittedCrossDomainPolicies: "none", + RobotTag: "noindex", }) app := secureMiddleware.Handler(myHandler) @@ -42,7 +44,7 @@ func main() { Be sure to include the Secure middleware as close to the top (beginning) as possible (but after logging and recovery). It's best to do the allowed hosts and SSL check before anything else. -The above example will only allow requests with a host name of 'example.com', or 'ssl.example.com'. Also if the request is not HTTPS, it will be redirected to HTTPS with the host name of 'ssl.example.com'. +The above example will only allow requests with a host name of 'example.com', or 'ssl.example.com'. Also, if the request is not HTTPS, it will be redirected to HTTPS with the host name of 'ssl.example.com'. Once those requirements are satisfied, it will add the following headers: ~~~ go Strict-Transport-Security: 31536000; includeSubdomains; preload @@ -53,7 +55,7 @@ Content-Security-Policy: script-src 'nonce-a2ZobGFoZg==' ~~~ ### Set the `IsDevelopment` option to `true` when developing! -When `IsDevelopment` is true, the AllowedHosts, SSLRedirect, and STS header will not be in effect. This allows you to work in development/test mode and not have any annoying redirects to HTTPS (ie. development can happen on HTTP), or block `localhost` has a bad host. +When `IsDevelopment` is true, the AllowedHosts, SSLRedirect, and STS header will not be in effect. This allows you to work in development/test mode and not have any annoying redirects to HTTPS (i.e. development can happen on HTTP), or block `localhost` has a bad host. ### Available options Secure comes with a variety of configuration options (Note: these are not the default option values. See the defaults below.): @@ -144,7 +146,7 @@ http.Error(w, "Bad Request", http.StatusBadRequest) Call `secure.SetBadRequestHandler` to set your own custom handler. ### Allow Request Function -Secure allows you to set a custom function (`func(r *http.Request) bool`) for the `AllowRequestFunc` option. You can use this function as a custom filter to allow the request to continue or simply reject it. This can be handy if you need to do any dynamic filtering on any of the request properties. It should be noted that this function will be called on every request, so be sure to make your checks quick and not relying on time consuming external calls (or you will be slowing down all requests). See above on how to set a custom handler for the rejected requests. +Secure allows you to set a custom function (`func(r *http.Request) bool`) for the `AllowRequestFunc` option. You can use this function as a custom filter to allow the request to continue or simply reject it. This can be handy if you need to do any dynamic filtering on any of the request properties. It should be noted that this function will be called on every request, so be sure to make your checks quick and not relying on time-consuming external calls (or you will be slowing down all requests). See above on how to set a custom handler for the rejected requests. ### Redirecting HTTP to HTTPS If you want to redirect all HTTP requests to HTTPS, you can use the following example. diff --git a/secure.go b/secure.go index 0ccbd8a..4f56c26 100644 --- a/secure.go +++ b/secure.go @@ -32,6 +32,9 @@ const ( corpHeader = "Cross-Origin-Resource-Policy" dnsPreFetchControlHeader = "X-DNS-Prefetch-Control" permittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" + robotTagHeader = "X-Robots-Tag" + permittedCrossDomainPoliciesHeader = "X-Permitted-Cross-Domain-Policies" + ctxDefaultSecureHeaderKey = secureCtxKey("SecureResponseHeader") cspNonceSize = 16 ) @@ -126,9 +129,15 @@ type Options struct { STSSeconds int64 // SecureContextKey allows a custom key to be specified for context storage. SecureContextKey string + // PermittedCrossDomainPolicies allows to set the X-Permitted-Cross-Domain-Policies header + // Reference https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers + PermittedCrossDomainPolicies string + // RobotTag allows to set the X-Robot-Tag header + // Reference https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag + RobotTag string } -// Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be +// Secure is a middleware that helps set up a few basic security features. A single secure.Options struct can be // provided to configure which features should be enabled, and the ability to override a few of the default values. type Secure struct { // Customize Secure with an Options struct. @@ -505,6 +514,16 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He responseHeader.Set(permittedCrossDomainPolicies, s.opt.XPermittedCrossDomainPolicies) } + // X-Permitted-Cross-Domain-Policies + if len(s.opt.PermittedCrossDomainPolicies) > 0 { + responseHeader.Set(permittedCrossDomainPoliciesHeader, s.opt.PermittedCrossDomainPolicies) + } + + // X-Robots-Tag + if len(s.opt.RobotTag) > 0 { + responseHeader.Set(robotTagHeader, s.opt.RobotTag) + } + return responseHeader, r, nil } diff --git a/secure_test.go b/secure_test.go index 0411d94..345a6b3 100644 --- a/secure_test.go +++ b/secure_test.go @@ -704,6 +704,36 @@ func TestStsHeaderWithSubdomainsWithPreloadForRequestOnly(t *testing.T) { expect(t, res.Header().Get("Strict-Transport-Security"), "") } +func TestXRobotTag(t *testing.T) { + s := New(Options{ + RobotTag: "foo", + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/foo", nil) + req.Host = "www.example.com" + + s.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("X-Robots-Tag"), "foo") +} + +func TestPermittedCrossDomainPolicies(t *testing.T) { + s := New(Options{ + PermittedCrossDomainPolicies: "foo", + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/foo", nil) + req.Host = "www.example.com" + + s.Handler(myHandler).ServeHTTP(res, req) + + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get("X-Permitted-Cross-Domain-Policies"), "foo") +} + func TestFrameDeny(t *testing.T) { s := New(Options{ FrameDeny: true, From da054b4c02ded2e4b3d164118298735458410ea0 Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Wed, 11 Mar 2026 17:44:05 +0100 Subject: [PATCH 4/4] fix: typo Signed-off-by: Christian Richter --- cspbuilder/builder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cspbuilder/builder_test.go b/cspbuilder/builder_test.go index c74ad98..7d691ab 100644 --- a/cspbuilder/builder_test.go +++ b/cspbuilder/builder_test.go @@ -127,7 +127,7 @@ func TestContentSecurityPolicyBuilder_Build_MultipleDirectives(t *testing.T) { } if strings.HasSuffix(got, " ") { - t.Errorf("ContentSecurityPolicyBuilder.Bui ld() = '%v', ends on whitespace", got) + t.Errorf("ContentSecurityPolicyBuilder.Build() = '%v', ends on whitespace", got) } if strings.HasSuffix(got, ";") {