Repository: google/go-safeweb
Pull request #247 (2021-02-08 12:03:53)

Added a `defaults` package.
For now it contains just a factory for safe ServeMuxConfigs.


Patch:
diff --git a/examples/echo/security/web/web.go b/examples/echo/security/web/web.go
index 4d51b4f..4c8f125 100644
--- a/examples/echo/security/web/web.go
+++ b/examples/echo/security/web/web.go
@@ -49,7 +49,7 @@ func NewMuxConfig() *safehttp.ServeMuxConfig {
 
 	c.Intercept(coop.Default(""))
 	c.Intercept(csp.Default(""))
-	c.Intercept(fetchmetadata.NewPlugin(""))
+	c.Intercept(fetchmetadata.NewInterceptor(""))
 	c.Intercept(staticheaders.Interceptor{})
 	return c
 }
diff --git a/safehttp/defaults/defaults.go b/safehttp/defaults/defaults.go
new file mode 100644
index 0000000..e9f03f5
--- /dev/null
+++ b/safehttp/defaults/defaults.go
@@ -0,0 +1,56 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Pacakge defaults provides ready to use, safe, pre-configured instances of safehttp types.
+package defaults
+
+import (
+	"errors"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/coop"
+	"github.com/google/go-safeweb/safehttp/plugins/csp"
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/hostcheck"
+	"github.com/google/go-safeweb/safehttp/plugins/hsts"
+	"github.com/google/go-safeweb/safehttp/plugins/staticheaders"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf/xsrfhtml"
+)
+
+// ServeMuxConfig creates a safe and ready to use ServeMuxConfig with all necessary security interceptors installed.
+// hosts should be all the hosts this mux will be served on and it can't be empty.
+// xsrfKey is the secret application key to use for XSRF token generation and it can't be empty.
+func ServeMuxConfig(hosts []string, xsrfKey string) (*safehttp.ServeMuxConfig, error) {
+
+	if len(hosts) == 0 {
+		return nil, errors.New("hosts slice cannot be empty")
+	}
+
+	if xsrfKey == "" {
+		return nil, errors.New("xsrfKey cannot be empty")
+	}
+
+	var c safehttp.ServeMuxConfig
+	// TODO(clap): add a report group once we support reporting.
+	c.Intercept(coop.Default(""))
+	// TODO(clap): add a report-uri once we support reporting.
+	// TODO(clap): find a way to make the FramingPolicy here work together with the Framing plugin once we have it.
+	c.Intercept(csp.Default(""))
+	c.Intercept(fetchmetadata.NewInterceptor())
+	c.Intercept(hostcheck.New(hosts...))
+	c.Intercept(hsts.Default())
+	c.Intercept(staticheaders.Interceptor{})
+	c.Intercept(&xsrfhtml.Interceptor{SecretAppKey: xsrfKey})
+	return &c, nil
+}
diff --git a/safehttp/plugins/fetchmetadata/fetchmetadata.go b/safehttp/plugins/fetchmetadata/fetchmetadata.go
index 53b685b..9bb0b5a 100644
--- a/safehttp/plugins/fetchmetadata/fetchmetadata.go
+++ b/safehttp/plugins/fetchmetadata/fetchmetadata.go
@@ -65,11 +65,11 @@ type Plugin struct {
 
 var _ safehttp.Interceptor = &Plugin{}
 
-// NewPlugin creates a new Fetch Metadata plugin in enforce mode that will apply
+// NewInterceptor creates a new Fetch Metadata plugin in enforce mode that will apply
 // the Resource Isolation Policy by default. The user can provide a set of
-// endpoints that are CORS-protected. Any request targeted to those endpoints
+// endpoints that will not be protected. Any request targeted to those endpoints
 // will be allowed by default without the policies being applied.
-func NewPlugin(endpoints ...string) *Plugin {
+func NewInterceptor(endpoints ...string) *Plugin {
 	m := map[string]bool{}
 	for _, e := range endpoints {
 		m[e] = true
diff --git a/safehttp/plugins/staticheaders/staticheaders.go b/safehttp/plugins/staticheaders/staticheaders.go
index 9557e10..0c2733a 100644
--- a/safehttp/plugins/staticheaders/staticheaders.go
+++ b/safehttp/plugins/staticheaders/staticheaders.go
@@ -33,6 +33,7 @@ import (
 )
 
 // Interceptor claims and sets static headers on responses.
+// The zero value is valid and ready to use.
 type Interceptor struct{}
 
 var _ safehttp.Interceptor = Interceptor{}


Test patch:
diff --git a/safehttp/defaults/defaults_test.go b/safehttp/defaults/defaults_test.go
new file mode 100644
index 0000000..c63e732
--- /dev/null
+++ b/safehttp/defaults/defaults_test.go
@@ -0,0 +1,57 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package defaults
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+func TestServeMuxConfig(t *testing.T) {
+	t.Run("can load", func(t *testing.T) {
+		const resp = "response"
+		cfg, _ := ServeMuxConfig([]string{"test.host.example"}, "test-xsrf-key")
+		cfg.Handle("/test", "GET", safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+			form, err := r.URL.Query()
+			if err != nil {
+				t.Errorf("Cannot parse GET form: %v", err)
+			}
+			b := form.Bool("test", false)
+			if !b {
+				t.Error("test parameter, got false, want true")
+			}
+			return w.Write(safehtml.HTMLEscaped(resp))
+		}))
+		mux := cfg.Mux()
+		w := httptest.NewRecorder()
+		r := httptest.NewRequest("GET", "https://test.host.example/test?test=true", nil)
+		mux.ServeHTTP(w, r)
+		if w.Result().StatusCode != 200 {
+			t.Errorf("Status: got %d, want 200", w.Result().StatusCode)
+		}
+		got, err := ioutil.ReadAll(w.Result().Body)
+		if err != nil {
+			t.Errorf("Read body: got %v", err)
+		}
+		if bytes.Compare(got, []byte(resp)) != 0 {
+			t.Errorf("body: got %q, want %q", got, resp)
+		}
+	})
+}
diff --git a/safehttp/plugins/fetchmetadata/fetchmetadata_test.go b/safehttp/plugins/fetchmetadata/fetchmetadata_test.go
index 33d9358..c7b7f3c 100644
--- a/safehttp/plugins/fetchmetadata/fetchmetadata_test.go
+++ b/safehttp/plugins/fetchmetadata/fetchmetadata_test.go
@@ -168,7 +168,7 @@ func TestAllowedResourceIsolationEnforceMode(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin()
+			p := fetchmetadata.NewInterceptor()
 			p.Before(rec.ResponseWriter, req, nil)
 
 			if want, got := safehttp.StatusOK, safehttp.StatusCode(rec.Status()); got != want {
@@ -194,7 +194,7 @@ func TestRejectedResourceIsolationEnforceMode(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin()
+			p := fetchmetadata.NewInterceptor()
 			p.Before(rec.ResponseWriter, req, nil)
 
 			if want, got := safehttp.StatusForbidden, safehttp.StatusCode(rec.Status()); want != got {
@@ -246,7 +246,7 @@ func TestRejectedResourceIsolationEnforceModeWithLogger(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin()
+			p := fetchmetadata.NewInterceptor()
 			logger := &methodLogger{}
 			p.Logger = logger
 			p.Before(rec.ResponseWriter, req, nil)
@@ -301,7 +301,7 @@ func TestResourceIsolationReportMode(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin()
+			p := fetchmetadata.NewInterceptor()
 			logger := &methodLogger{}
 			p.Logger = logger
 			p.SetReportOnly()
@@ -324,7 +324,7 @@ func TestResourceIsolationReportMode(t *testing.T) {
 }
 
 func TestReportModeMissingLogger(t *testing.T) {
-	p := fetchmetadata.NewPlugin()
+	p := fetchmetadata.NewInterceptor()
 	defer func() {
 		if r := recover(); r != nil {
 			return
@@ -344,7 +344,7 @@ func TestNavIsolationEnforceMode(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin()
+			p := fetchmetadata.NewInterceptor()
 			p.NavIsolation = true
 			p.Before(rec.ResponseWriter, req, nil)
 
@@ -398,7 +398,7 @@ func TestNavIsolationReportMode(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin()
+			p := fetchmetadata.NewInterceptor()
 			logger := &methodLogger{}
 			p.Logger = logger
 			p.NavIsolation = true
@@ -431,7 +431,7 @@ func TestCORSEndpoint(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin("/carbonara")
+			p := fetchmetadata.NewInterceptor("/carbonara")
 			p.Before(rec.ResponseWriter, req, nil)
 
 			if want, got := safehttp.StatusOK, safehttp.StatusCode(rec.Status()); got != want {
@@ -497,7 +497,7 @@ func TestCORSAfterRedirect(t *testing.T) {
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			rec := safehttptest.NewResponseRecorder()
 
-			p := fetchmetadata.NewPlugin("/carbonara")
+			p := fetchmetadata.NewInterceptor("/carbonara")
 			p.NavIsolation = true
 			p.RedirectURL, _ = safehttp.ParseURL("https://spaghetti.com/carbonara")
 			p.Before(rec.ResponseWriter, req, nil)