Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: monthly
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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.):
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions cspbuilder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -60,15 +61,15 @@ 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
wantErr bool
}{
{
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"},
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions cspbuilder/directive_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func TestBuildDirectiveFrameAncestors(t *testing.T) {
sb := &strings.Builder{}

err := buildDirectiveFrameAncestors(sb, tt.values)

if tt.wantErr && err != nil {
return
}
Expand Down Expand Up @@ -225,6 +226,7 @@ func TestBuildDirectiveTrustedTypes(t *testing.T) {
sb := &strings.Builder{}

err := buildDirectiveTrustedTypes(sb, tt.values)

if tt.wantErr && err != nil {
return
}
Expand Down
30 changes: 25 additions & 5 deletions secure.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package secure

import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
Expand Down Expand Up @@ -31,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
)
Expand Down Expand Up @@ -125,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.
Expand Down Expand Up @@ -369,7 +379,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 {
Expand All @@ -395,15 +405,15 @@ 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 the AllowRequestFunc is set, call it and exit early if needed.
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.
Expand Down Expand Up @@ -504,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
}

Expand Down Expand Up @@ -535,7 +555,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)
}
Expand Down
30 changes: 30 additions & 0 deletions secure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down