Repository: google/go-safeweb (go)

Issue:
Make multipart forms resilient to path traversal attacks
See https://tools.ietf.org/html/rfc7578:

> If a "filename" parameter is supplied, the requirements of
> Section 2.3 of [RFC2183] for the "receiving MUA" (i.e., the receiving
> Mail User Agent) apply to receivers of multipart/form-data as well:
> do not use the file name blindly, check and possibly change to match
> local file system conventions if applicable, and do not use directory
> path information that may be present.

Pull request description:
Sanitise file names in multipart forms.
Fixes #264

Patch:
diff --git a/go.mod b/go.mod
index f0664bf..d40bacd 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/google/go-safeweb
 
-go 1.14
+go 1.16
 
 require (
 	github.com/google/go-cmp v0.5.0
diff --git a/safehttp/defaults/defaults.go b/safehttp/defaults/defaults.go
index e9f03f5..703bb0d 100644
--- a/safehttp/defaults/defaults.go
+++ b/safehttp/defaults/defaults.go
@@ -41,7 +41,7 @@ func ServeMuxConfig(hosts []string, xsrfKey string) (*safehttp.ServeMuxConfig, e
 		return nil, errors.New("xsrfKey cannot be empty")
 	}
 
-	var c safehttp.ServeMuxConfig
+	c := safehttp.NewServeMuxConfig(nil)
 	// TODO(clap): add a report group once we support reporting.
 	c.Intercept(coop.Default(""))
 	// TODO(clap): add a report-uri once we support reporting.
@@ -52,5 +52,5 @@ func ServeMuxConfig(hosts []string, xsrfKey string) (*safehttp.ServeMuxConfig, e
 	c.Intercept(hsts.Default())
 	c.Intercept(staticheaders.Interceptor{})
 	c.Intercept(&xsrfhtml.Interceptor{SecretAppKey: xsrfKey})
-	return &c, nil
+	return c, nil
 }
diff --git a/safehttp/fileserver_1_16.go b/safehttp/fileserver_1_16.go
new file mode 100644
index 0000000..f081e57
--- /dev/null
+++ b/safehttp/fileserver_1_16.go
@@ -0,0 +1,31 @@
+// 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.
+
+// +build go1.16
+
+package safehttp
+
+import (
+	"embed"
+	"net/http"
+)
+
+func FileServerEmbed(fs embed.FS) Handler {
+	fileServer := http.FileServer(http.FS(fs))
+	return HandlerFunc(func(rw ResponseWriter, req *IncomingRequest) Result {
+		fsrw := &fileServerResponseWriter{flight: rw.(*flight), header: http.Header{}}
+		fileServer.ServeHTTP(fsrw, req.req)
+		return fsrw.result
+	})
+}
diff --git a/safehttp/form.go b/safehttp/form.go
index 80fc1ad..9a8820b 100644
--- a/safehttp/form.go
+++ b/safehttp/form.go
@@ -17,6 +17,7 @@ package safehttp
 import (
 	"fmt"
 	"mime/multipart"
+	"path/filepath"
 	"strconv"
 )
 
@@ -204,6 +205,28 @@ type MultipartForm struct {
 	mf *multipart.Form
 }
 
+// newMultipartForm constructs a new MultipartForm with sanitized values.
+func newMulipartForm(mf *multipart.Form) *MultipartForm {
+	return &MultipartForm{
+		Form: Form{
+			values: mf.Value,
+		},
+		mf: sanitizeFilenames(mf),
+	}
+}
+
+// sanitizeFilenames removes trailing path separators from all file names.
+// This is to ensure that uploaded files are not stored outside of the
+// designated directory.
+func sanitizeFilenames(f *multipart.Form) *multipart.Form {
+	for _, f := range f.File {
+		for _, fh := range f {
+			fh.Filename = filepath.Base(fh.Filename)
+		}
+	}
+	return f
+}
+
 // File returns the file parts associated with form key param or a nil
 // slice, if none. These can be then opened individually by calling
 // FileHeader.Open.
diff --git a/safehttp/incoming_request.go b/safehttp/incoming_request.go
index 791f931..9da62bb 100644
--- a/safehttp/incoming_request.go
+++ b/safehttp/incoming_request.go
@@ -125,13 +125,7 @@ func (r *IncomingRequest) MultipartForm(maxMemory int64) (*MultipartForm, error)
 	if err != nil {
 		return nil, err
 	}
-	return &MultipartForm{
-			Form: Form{
-				values: r.req.MultipartForm.Value,
-			},
-			mf: r.req.MultipartForm,
-		},
-		nil
+	return newMulipartForm(r.req.MultipartForm), nil
 }
 
 // Cookie returns the named cookie provided in the request or
diff --git a/safehttp/mux.go b/safehttp/mux.go
index 6be5a07..39bb65e 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -111,11 +111,20 @@ func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 // ServeMuxConfig is a builder for ServeMux.
 type ServeMuxConfig struct {
-	Dispatcher   Dispatcher
+	dispatcher   Dispatcher
 	handlers     []handlerRegistration
 	interceptors []Interceptor
 }
 
+// NewServeMuxConfig crates a ServeMuxConfig with the provided Dispatcher. If
+// the provided Dispatcher is nil, the DefaultDispatcher is used.
+func NewServeMuxConfig(disp Dispatcher) *ServeMuxConfig {
+	if disp == nil {
+		disp = &DefaultDispatcher{}
+	}
+	return &ServeMuxConfig{dispatcher: disp}
+}
+
 type handlerRegistration struct {
 	pattern string
 	method  string
@@ -154,9 +163,8 @@ func (s *ServeMuxConfig) Mux() *ServeMux {
 		log.Println("Warning: creating safehttp.Mux in dev mode. This configuration is not valid for production use")
 	}
 
-	dispatcher := s.Dispatcher
-	if dispatcher == nil {
-		dispatcher = DefaultDispatcher{}
+	if s.dispatcher == nil {
+		panic("Use NewServeMuxConfig instead of creating ServeMuxConfig using a composite literal.")
 	}
 	// pattern -> method -> handlerConfig
 	handlers := map[string]map[string]handlerConfig{}
@@ -170,7 +178,7 @@ func (s *ServeMuxConfig) Mux() *ServeMux {
 		}
 		handlers[hr.pattern][hr.method] =
 			handlerConfig{
-				Dispatcher:   dispatcher,
+				Dispatcher:   s.dispatcher,
 				Handler:      hr.handler,
 				Interceptors: configureInterceptors(s.interceptors, hr.cfgs),
 			}
@@ -202,7 +210,7 @@ func configureInterceptors(interceptors []Interceptor, cfgs []InterceptorConfig)
 // plugins and some common handlers.
 func (s *ServeMuxConfig) Clone() *ServeMuxConfig {
 	c := &ServeMuxConfig{
-		Dispatcher:   s.Dispatcher,
+		dispatcher:   s.dispatcher,
 		handlers:     make([]handlerRegistration, len(s.handlers)),
 		interceptors: make([]Interceptor, len(s.interceptors)),
 	}


Test patch:
diff --git a/safehttp/fileserver_1_16_test.go b/safehttp/fileserver_1_16_test.go
new file mode 100644
index 0000000..65eea9c
--- /dev/null
+++ b/safehttp/fileserver_1_16_test.go
@@ -0,0 +1,91 @@
+// 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.
+
+// +build go1.16
+
+package safehttp_test
+
+import (
+	"embed"
+	"io"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+)
+
+//go:embed testdata
+var testEmbeddedFS embed.FS
+
+func TestFileServerEmbed(t *testing.T) {
+	wantFile, err := testEmbeddedFS.Open("testdata/embed.html")
+	if err != nil {
+		t.Fatalf("Could not open embedded test files: %v", err)
+	}
+	wantFileContent, err := io.ReadAll(wantFile)
+	if err != nil {
+		t.Fatalf("Could not read embedded test files: %v", err)
+	}
+
+	tests := []struct {
+		name     string
+		path     string
+		wantCode safehttp.StatusCode
+		wantCT   string
+		wantBody string
+	}{
+		{
+			name:     "missing file",
+			path:     "failure",
+			wantCode: 404,
+			wantCT:   "text/plain; charset=utf-8",
+			wantBody: "Not Found\n",
+		},
+		{
+			name:     "embedded file",
+			path:     "testdata/embed.html",
+			wantCode: 200,
+			wantCT:   "text/html; charset=utf-8",
+			wantBody: string(wantFileContent),
+		},
+	}
+
+	mb := &safehttp.ServeMuxConfig{}
+	mb.Handle("/", safehttp.MethodGet, safehttp.FileServerEmbed(testEmbeddedFS))
+	m := mb.Mux()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			rr := httptest.NewRecorder()
+
+			req := httptest.NewRequest(safehttp.MethodGet, "https://test.science/"+tt.path, nil)
+			m.ServeHTTP(rr, req)
+
+			if got, want := rr.Result().StatusCode, tt.wantCode; safehttp.StatusCode(got) != tt.wantCode {
+				t.Errorf("status code got: %v want: %v", got, want)
+			}
+			if got := rr.Header().Get("Content-Type"); tt.wantCT != got {
+				t.Errorf("Content-Type: got %q want %q", got, tt.wantCT)
+			}
+			body, err := io.ReadAll(rr.Result().Body)
+			if err != nil {
+				t.Errorf("Can't read response body: %v", err)
+			}
+			if diff := cmp.Diff(tt.wantBody, string(body)); diff != "" {
+				t.Errorf("Response body diff (-want,+got): \n%s", diff)
+			}
+		})
+	}
+}
diff --git a/safehttp/fileserver_test.go b/safehttp/fileserver_test.go
index df7cd05..de19439 100644
--- a/safehttp/fileserver_test.go
+++ b/safehttp/fileserver_test.go
@@ -58,7 +58,7 @@ func TestFileServer(t *testing.T) {
 		},
 	}
 
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/", safehttp.MethodGet, safehttp.FileServer(tmpDir))
 	m := mb.Mux()
 
diff --git a/safehttp/flight_test.go b/safehttp/flight_test.go
index ed0a8c4..4a85b0e 100644
--- a/safehttp/flight_test.go
+++ b/safehttp/flight_test.go
@@ -59,7 +59,7 @@ func TestFlightInterceptorPanic(t *testing.T) {
 	}
 	for _, tc := range tests {
 		t.Run(tc.desc, func(t *testing.T) {
-			mb := &safehttp.ServeMuxConfig{}
+			mb := safehttp.NewServeMuxConfig(nil)
 			mb.Intercept(tc.interceptor)
 			mb.Handle("/search", safehttp.MethodGet, safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				// IMPORTANT: We are setting the header here and expecting to be
@@ -94,7 +94,7 @@ func TestFlightInterceptorPanic(t *testing.T) {
 }
 
 func TestFlightHandlerPanic(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/search", safehttp.MethodGet, safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		// IMPORTANT: We are setting the header here and expecting to be
 		// cleared if a panic occurs.
@@ -136,7 +136,7 @@ func TestFlightDoubleWritePanics(t *testing.T) {
 	for firstWriteName, firstWrite := range writeFuncs {
 		for secondWriteName, secondWrite := range writeFuncs {
 			t.Run(fmt.Sprintf("%s->%s", firstWriteName, secondWriteName), func(t *testing.T) {
-				mb := &safehttp.ServeMuxConfig{}
+				mb := safehttp.NewServeMuxConfig(nil)
 				mb.Handle("/search", safehttp.MethodGet, safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 					firstWrite(w, r)
 					secondWrite(w, r) // this should panic
diff --git a/safehttp/form_test.go b/safehttp/form_test.go
index c119359..bdd68d0 100644
--- a/safehttp/form_test.go
+++ b/safehttp/form_test.go
@@ -505,11 +505,11 @@ func TestFormClearSliceUnknownType(t *testing.T) {
 
 func TestMultipartFormValidFile(t *testing.T) {
 	fh := &multipart.FileHeader{Filename: "bar"}
-	f := &MultipartForm{mf: &multipart.Form{
+	mf := &multipart.Form{
 		File: map[string][]*multipart.FileHeader{
 			"foo": {fh},
-		},
-	}}
+		}}
+	f := newMulipartForm(mf)
 	fhs := f.File("foo")
 	if fhs == nil {
 		t.Errorf(`m.File("foo"): got nil, want file headers`)
@@ -521,14 +521,12 @@ func TestMultipartFormValidFile(t *testing.T) {
 
 func TestMultipartFormValidFileAndVals(t *testing.T) {
 	fh := &multipart.FileHeader{Filename: "bar"}
-	f := &MultipartForm{
-		Form: Form{values: map[string][]string{"number": {"1"}}},
-		mf: &multipart.Form{
-			File: map[string][]*multipart.FileHeader{
-				"foo": {fh},
-			},
-		},
-	}
+	mf := &multipart.Form{
+		Value: map[string][]string{"number": {"1"}},
+		File: map[string][]*multipart.FileHeader{
+			"foo": {fh},
+		}}
+	f := newMulipartForm(mf)
 
 	if want, got := int64(1), f.Int64("number", 0); want != got {
 		t.Errorf(`f.Int64("number"): got %d, want %d`, got, want)
@@ -546,8 +544,25 @@ func TestMultipartFormValidFileAndVals(t *testing.T) {
 	}
 }
 
+func TestMultipartFormFileWithPathInName(t *testing.T) {
+	fh := &multipart.FileHeader{Filename: "../tmp/myfile.txt"}
+	mf := &multipart.Form{
+		File: map[string][]*multipart.FileHeader{
+			"foo": {fh},
+		}}
+	f := newMulipartForm(mf)
+
+	filename := f.File("foo")[0].Filename
+	if want, got := "myfile.txt", filename; want != got {
+		t.Errorf(`f.File("foo").Filename: got %q, want %q`, got, want)
+	}
+	if err := f.Err(); err != nil {
+		t.Errorf(`f.Err(): got err %v, want nil`, err)
+	}
+}
+
 func TestMultipartFormMissingFile(t *testing.T) {
-	f := &MultipartForm{mf: &multipart.Form{}}
+	f := newMulipartForm(&multipart.Form{})
 	fhs := f.File("x")
 	if fhs != nil {
 		t.Errorf(`m.File("x"): got file headers, want nil`)
diff --git a/safehttp/mux_test.go b/safehttp/mux_test.go
index d4d0f28..c29037b 100644
--- a/safehttp/mux_test.go
+++ b/safehttp/mux_test.go
@@ -56,7 +56,7 @@ func TestMuxOneHandlerOneRequest(t *testing.T) {
 
 	for _, tt := range test {
 		t.Run(tt.name, func(t *testing.T) {
-			mb := &safehttp.ServeMuxConfig{}
+			mb := safehttp.NewServeMuxConfig(nil)
 
 			h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
@@ -118,7 +118,7 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 		},
 	}
 
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/bar", safehttp.MethodGet, tests[0].hf)
 	mb.Handle("/bar", safehttp.MethodPost, tests[1].hf)
 	mux := mb.Mux()
@@ -165,7 +165,7 @@ func TestMuxRegisterCorrectHandlerAllPaths(t *testing.T) {
 		},
 	}
 
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/get", safehttp.MethodGet, tests[0].hf)
 	mb.Handle("/get2", safehttp.MethodGet, tests[1].hf)
 	mux := mb.Mux()
@@ -181,7 +181,7 @@ func TestMuxRegisterCorrectHandlerAllPaths(t *testing.T) {
 }
 
 func TestMuxHandleSameMethodTwice(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 
 	registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
@@ -272,7 +272,7 @@ func TestMuxInterceptors(t *testing.T) {
 		{
 			name: "Install ServeMux Interceptor before handler registration",
 			mux: func() *safehttp.ServeMux {
-				mb := &safehttp.ServeMuxConfig{}
+				mb := safehttp.NewServeMuxConfig(nil)
 				mb.Intercept(setHeaderInterceptor{
 					name:  "Foo",
 					value: "bar",
@@ -294,7 +294,7 @@ func TestMuxInterceptors(t *testing.T) {
 		{
 			name: "Install Interrupting Interceptor",
 			mux: func() *safehttp.ServeMux {
-				mb := &safehttp.ServeMuxConfig{}
+				mb := safehttp.NewServeMuxConfig(nil)
 				mb.Intercept(internalErrorInterceptor{})
 
 				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
@@ -314,7 +314,7 @@ func TestMuxInterceptors(t *testing.T) {
 		{
 			name: "Handler Communication With ServeMux Interceptor",
 			mux: func() *safehttp.ServeMux {
-				mb := &safehttp.ServeMuxConfig{}
+				mb := safehttp.NewServeMuxConfig(nil)
 				mb.Intercept(&claimHeaderInterceptor{headerToClaim: "Foo"})
 
 				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
@@ -335,7 +335,7 @@ func TestMuxInterceptors(t *testing.T) {
 		{
 			name: "Commit phase sets header",
 			mux: func() *safehttp.ServeMux {
-				mb := &safehttp.ServeMuxConfig{}
+				mb := safehttp.NewServeMuxConfig(nil)
 				mb.Intercept(committerInterceptor{})
 
 				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
@@ -450,7 +450,7 @@ func TestMuxInterceptorConfigs(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			mb := &safehttp.ServeMuxConfig{}
+			mb := safehttp.NewServeMuxConfig(nil)
 			mb.Intercept(setHeaderConfigInterceptor{})
 
 			registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
@@ -529,7 +529,7 @@ func (interceptorThree) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.Inc
 }
 
 func TestMuxDeterministicInterceptorOrder(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Intercept(interceptorOne{})
 	mb.Intercept(interceptorTwo{})
 	mb.Intercept(interceptorThree{})
@@ -567,7 +567,7 @@ func TestMuxDeterministicInterceptorOrder(t *testing.T) {
 }
 
 func TestMuxHandlerReturnsNotWritten(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		return safehttp.NotWritten()
 	})
diff --git a/safehttp/plugins/hostcheck/hostcheck_test.go b/safehttp/plugins/hostcheck/hostcheck_test.go
index 5aec398..dcc753f 100644
--- a/safehttp/plugins/hostcheck/hostcheck_test.go
+++ b/safehttp/plugins/hostcheck/hostcheck_test.go
@@ -44,7 +44,7 @@ func TestInterceptor(t *testing.T) {
 
 	for _, tt := range test {
 		t.Run(tt.name, func(t *testing.T) {
-			mb := &safehttp.ServeMuxConfig{}
+			mb := safehttp.NewServeMuxConfig(nil)
 			mb.Intercept(hostcheck.New("foo.com"))
 
 			h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
diff --git a/safehttp/server_test.go b/safehttp/server_test.go
index 748f4ea..be6d9d7 100644
--- a/safehttp/server_test.go
+++ b/safehttp/server_test.go
@@ -30,7 +30,7 @@ func TestServer(t *testing.T) {
 	l := requesttesting.NewFakeListener()
 	defer l.Close()
 
-	var cfg ServeMuxConfig
+	cfg := NewServeMuxConfig(nil)
 	cfg.Handle("/", "GET", HandlerFunc(func(w ResponseWriter, r *IncomingRequest) Result {
 		return w.Write(safehtml.HTMLEscaped("response"))
 	}))
diff --git a/safehttp/testdata/embed.html b/safehttp/testdata/embed.html
new file mode 100644
index 0000000..356936c
--- /dev/null
+++ b/safehttp/testdata/embed.html
@@ -0,0 +1,19 @@
+<!--
+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.
+-->
+<!DOCTYPE html>
+<html>
+  <body>
+    <h1> This is a test embedded document </h1>
+  </body>
+</html>
diff --git a/tests/integration/csp/csp_test.go b/tests/integration/csp/csp_test.go
index a447c9c..a7f6987 100644
--- a/tests/integration/csp/csp_test.go
+++ b/tests/integration/csp/csp_test.go
@@ -25,7 +25,7 @@ import (
 )
 
 func TestServeMuxInstallCSP(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	it := csp.Default("")
 	mb.Intercept(&it)
 
diff --git a/tests/integration/errors/errors_test.go b/tests/integration/errors/errors_test.go
index 3b23583..8797182 100644
--- a/tests/integration/errors/errors_test.go
+++ b/tests/integration/errors/errors_test.go
@@ -52,7 +52,7 @@ func (m myDispatcher) Error(rw http.ResponseWriter, resp safehttp.ErrorResponse)
 // custom dispatcher implementation. XSRF interceptor is added to check that
 // error responses go through the commit phase.
 func TestCustomErrors(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{Dispatcher: myDispatcher{}}
+	mb := safehttp.NewServeMuxConfig(myDispatcher{})
 
 	mb.Handle("/compute", safehttp.MethodGet, safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		qs, err := r.URL.Query()
@@ -121,7 +121,7 @@ func (it interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.Incom
 }
 
 func TestCustomErrorsInBefore(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{Dispatcher: myDispatcher{}}
+	mb := safehttp.NewServeMuxConfig(myDispatcher{})
 	mb.Intercept(interceptor{errBefore: true})
 
 	mb.Handle("/compute", safehttp.MethodGet, safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
diff --git a/tests/integration/header/header_test.go b/tests/integration/header/header_test.go
index 466dbe6..445e4a3 100644
--- a/tests/integration/header/header_test.go
+++ b/tests/integration/header/header_test.go
@@ -27,7 +27,7 @@ import (
 )
 
 func TestAccessIncomingHeaders(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		if got, want := ir.Header.Get("A"), "B"; got != want {
 			t.Errorf(`ir.Header.Get("A") got: %v want: %v`, got, want)
@@ -49,7 +49,7 @@ func TestAccessIncomingHeaders(t *testing.T) {
 }
 
 func TestChangingResponseHeaders(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		rw.Header().Set("pIZZA", "Pasta")
 		return rw.Write(safehtml.HTMLEscaped("hello"))
diff --git a/tests/integration/hsts/hsts_test.go b/tests/integration/hsts/hsts_test.go
index 2697726..144e26e 100644
--- a/tests/integration/hsts/hsts_test.go
+++ b/tests/integration/hsts/hsts_test.go
@@ -26,7 +26,7 @@ import (
 )
 
 func TestHSTSServeMuxInstall(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 
 	mb.Intercept(hsts.Default())
 	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
@@ -58,7 +58,7 @@ func TestHSTSServeMuxInstall(t *testing.T) {
 }
 
 func TestHSTSOnErrors(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 
 	mb.Intercept(hsts.Default())
 	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
diff --git a/tests/integration/mux/mux_test.go b/tests/integration/mux/mux_test.go
index 338760e..e968a95 100644
--- a/tests/integration/mux/mux_test.go
+++ b/tests/integration/mux/mux_test.go
@@ -71,7 +71,7 @@ func TestMuxDefaultDispatcher(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			mb := &safehttp.ServeMuxConfig{}
+			mb := safehttp.NewServeMuxConfig(nil)
 			mb.Handle("/pizza", safehttp.MethodGet, tt.handler)
 
 			rw := httptest.NewRecorder()
@@ -127,7 +127,7 @@ func TestMuxDefaultDispatcherUnsafeResponses(t *testing.T) {
 			// error-handling in the ResponseWriter has been fixed.
 			t.Skip()
 
-			mb := &safehttp.ServeMuxConfig{}
+			mb := safehttp.NewServeMuxConfig(nil)
 			mb.Handle("/pizza", safehttp.MethodGet, tt.handler)
 			mux := mb.Mux()
 
diff --git a/tests/integration/safehtml/safehtml_test.go b/tests/integration/safehtml/safehtml_test.go
index c46f71a..4a3fcca 100644
--- a/tests/integration/safehtml/safehtml_test.go
+++ b/tests/integration/safehtml/safehtml_test.go
@@ -24,7 +24,7 @@ import (
 )
 
 func TestHandleRequestWrite(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		return rw.Write(safehtml.HTMLEscaped("<h1>Escaped, so not really a heading</h1>"))
 	}))
@@ -43,7 +43,7 @@ func TestHandleRequestWrite(t *testing.T) {
 }
 
 func TestHandleRequestWriteTemplate(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	mb.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		return safehttp.ExecuteTemplate(rw, template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
 	}))
diff --git a/tests/integration/staticheaders/staticheaders_test.go b/tests/integration/staticheaders/staticheaders_test.go
index b730e9f..6e5fbd4 100644
--- a/tests/integration/staticheaders/staticheaders_test.go
+++ b/tests/integration/staticheaders/staticheaders_test.go
@@ -26,7 +26,7 @@ import (
 )
 
 func TestServeMuxInstallStaticHeaders(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 
 	mb.Intercept(staticheaders.Interceptor{})
 	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
@@ -59,7 +59,7 @@ func TestServeMuxInstallStaticHeaders(t *testing.T) {
 }
 
 func TestStaticHeadersOnError(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 
 	mb.Intercept(staticheaders.Interceptor{})
 	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
diff --git a/tests/integration/xsrf/xsrf_test.go b/tests/integration/xsrf/xsrf_test.go
index 7617b29..02177c5 100644
--- a/tests/integration/xsrf/xsrf_test.go
+++ b/tests/integration/xsrf/xsrf_test.go
@@ -25,7 +25,7 @@ import (
 )
 
 func TestServeMuxInstallXSRF(t *testing.T) {
-	mb := &safehttp.ServeMuxConfig{}
+	mb := safehttp.NewServeMuxConfig(nil)
 	it := xsrfhtml.Interceptor{SecretAppKey: "testSecretAppKey"}
 	mb.Intercept(&it)