Repository: google/go-safeweb (go)

Issue:
The dispatcher can write a response without providing a Content-Type header.
Currently the `safehttp.Dispatcher` can write a response without providing the `Content-Type` header. This could potentially lead to security issues. We therefore want to ensure that a `Content-Type` header is always written on each response.

This could be accomplished by creating a wrapper around the `http.ResponseWriter` such that the dispatcher needs to first specify a `Content-Type` before getting access to the responsewriter.

Pull request description:
Extended the Dispatcher interface to support writing JSON and retrieving the response Content-Type and created a safe default Dispatcher.
Fixes #60, #133

Extended the Dispatcher interface to support writing JSON safe responses and retrieving the Content-Type of safe response and added a . This way the ResponseWriter can query the Dispatcher to retrieve and set the Content-Type of a safe response before writing the response. Implemented and write tests for a default safe dispatcher.

Patch:
diff --git a/safehttp/default_dispatcher.go b/safehttp/default_dispatcher.go
new file mode 100644
index 0000000..ba028e1
--- /dev/null
+++ b/safehttp/default_dispatcher.go
@@ -0,0 +1,76 @@
+// 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 safehttp
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/google/safehtml"
+	"github.com/google/safehtml/template"
+	"io"
+	"net/http"
+)
+
+// DefaultDispatcher is responsible for writing safe responses.
+type DefaultDispatcher struct{}
+
+// ContentType returns the Content-Type of a safe response if it's of a
+// safe-type and an error otherwise.
+func (DefaultDispatcher) ContentType(resp Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", fmt.Errorf("%T is not a safe response type, a Content-Type cannot be provided", resp)
+	}
+}
+
+// Write writes the response to the Response Writer if it's safe HTML. It
+// returns a non-nil error if the response is not safe HTML or if writing the
+// response fails.
+func (DefaultDispatcher) Write(rw http.ResponseWriter, resp Response) error {
+	x, ok := resp.(safehtml.HTML)
+	if !ok {
+		return fmt.Errorf("%T is not a safe response type and it cannot be written", resp)
+	}
+	_, err := io.WriteString(rw, x.String())
+	return err
+
+}
+
+// WriteJSON serialises and writes a JSON to the underlying http.ResponseWriter.
+// If the provided response is not valid JSON or writing the response fails, the
+// method will return an error.
+func (DefaultDispatcher) WriteJSON(rw http.ResponseWriter, resp JSONResponse) error {
+	io.WriteString(rw, ")]}',\n") // Break parsing of JavaScript in order to prevent XSSI.
+	return json.NewEncoder(rw).Encode(resp.Data)
+}
+
+// ExecuteTemplate applies a parsed template to the provided data object if the
+// template is a safe HTML template, writing the output to the  http.
+// ResponseWriter.
+//
+// If an error occurs executing the template or writing its output,
+// execution stops, but partial results may already have been written to
+// the Response Writer.
+func (DefaultDispatcher) ExecuteTemplate(rw http.ResponseWriter, t Template, data interface{}) error {
+	x, ok := t.(*template.Template)
+	if !ok {
+		return fmt.Errorf("%T is not a safe template and it cannot be parsed and written", t)
+	}
+	return x.Execute(rw, data)
+}
diff --git a/safehttp/mux.go b/safehttp/mux.go
index b99582f..15e3ec7 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -80,8 +80,13 @@ type ServeMux struct {
 	interceps []Interceptor
 }
 
-// NewServeMux allocates and returns a new ServeMux
+// NewServeMux allocates and returns a new ServeMux. Passing a nil Dispatcher
+// will cause the method to panic.
 func NewServeMux(d Dispatcher, domains ...string) *ServeMux {
+	if d == nil {
+		d = DefaultDispatcher{}
+	}
+
 	// TODO(@mattiasgrenfeldt, @mihalimara22): make domains a variadic of string **literals**.
 	dm := map[string]bool{}
 	for _, host := range domains {
diff --git a/safehttp/plugins/fetch_metadata/fetch_metadata.go b/safehttp/plugins/fetch_metadata/fetch_metadata.go
index 5b51efd..70a256b 100644
--- a/safehttp/plugins/fetch_metadata/fetch_metadata.go
+++ b/safehttp/plugins/fetch_metadata/fetch_metadata.go
@@ -12,6 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Package fetchmetadata provides a safehttp.Interceptor that applies Fetch
+// Metadata policies to incoming requests in order to protect applications
+// against cross-origin attacks.
 package fetchmetadata
 
 import (
diff --git a/safehttp/plugins/xsrf/xsrf.go b/safehttp/plugins/xsrf/xsrf.go
index ffc3c44..5d48fdd 100644
--- a/safehttp/plugins/xsrf/xsrf.go
+++ b/safehttp/plugins/xsrf/xsrf.go
@@ -12,6 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Package xsrf provides a safehttp.Interceptor that ensures Cross-Site Request
+// Forgery protection by verifying the incoming requests, rejecting those
+// requests that are suspected to be part of an attack.
 package xsrf
 
 import (
diff --git a/safehttp/response.go b/safehttp/response.go
index 75ec422..c2e4980 100644
--- a/safehttp/response.go
+++ b/safehttp/response.go
@@ -21,6 +21,12 @@ import "io"
 // supported by the Dispatcher.
 type Response interface{}
 
+// JSONResponse should encapsulate a valid JSON object that will be serialised
+// and written to the http.ResponseWriter using a JSON encoder.
+type JSONResponse struct {
+	Data interface{}
+}
+
 // Template implements a template.
 type Template interface {
 	// Execute applies data to the template and then writes the result to
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index b499dca..83f6b3a 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -66,7 +66,12 @@ func NotWritten() Result {
 	return Result{}
 }
 
-// Write TODO
+// Write dispatches the response to the Dispatcher, setting the Content-Type and
+// response status if the provided response is a safe response. The
+// Dispatcher will then write the response to the underlying Response Writer.
+//
+// TODO: replace panics with proper error handling when getting the response
+// Content-Type or writing the response fails.
 func (w *ResponseWriter) Write(resp Response) Result {
 	if w.written {
 		panic("ResponseWriter was already written to")
@@ -76,13 +81,53 @@ func (w *ResponseWriter) Write(resp Response) Result {
 		return Result{}
 	}
 	w.markWritten()
+	ct, err := w.d.ContentType(resp)
+	if err != nil {
+		panic(err)
+	}
+	w.rw.WriteHeader(int(StatusOK))
+	w.rw.Header().Set("Content-Type", ct)
 	if err := w.d.Write(w.rw, resp); err != nil {
-		panic("error")
+		panic(err)
+	}
+	return Result{}
+}
+
+// WriteJSON encapsulates data into a JSON response and dispatches it to the
+// Dispatcher, to be serialised and written to the ResponseWriter. It will also
+// set the Content-Type and response status will be set.
+//
+// TODO: replace panics with proper error handling when getting the response
+// Content-Type or writing the response fails.
+func (w *ResponseWriter) WriteJSON(data interface{}) Result {
+	if w.written {
+		panic("ResponseWriter was already written to")
+	}
+	w.handler.commitPhase(w, JSONResponse{Data: data})
+	if w.written {
+		return Result{}
+	}
+	w.markWritten()
+	resp := JSONResponse{Data: data}
+	ct, err := w.d.ContentType(resp)
+	if err != nil {
+		panic(err)
+	}
+	w.rw.WriteHeader(int(StatusOK))
+	w.rw.Header().Set("Content-Type", ct)
+	if err := w.d.WriteJSON(w.rw, resp); err != nil {
+		panic(err)
 	}
 	return Result{}
 }
 
-// WriteTemplate TODO
+// WriteTemplate dispatches a parsed template and a data object to the
+// Dispatcher to be executed and written to the underlying Response Writer, in
+// case the template is a safe HTML template. If it is a safe HTML Template, the
+// Content-Type  and response status will also be set.
+//
+// TODO: replace panics with proper error handling when getting the response
+// Content-Type or writing the response fails.
 func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 	if w.written {
 		panic("ResponseWriter was already written to")
@@ -92,8 +137,14 @@ func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 		return Result{}
 	}
 	w.markWritten()
+	ct, err := w.d.ContentType(t)
+	if err != nil {
+		panic(err)
+	}
+	w.rw.WriteHeader(int(StatusOK))
+	w.rw.Header().Set("Content-Type", ct)
 	if err := w.d.ExecuteTemplate(w.rw, t, data); err != nil {
-		panic("error")
+		panic(err)
 	}
 	return Result{}
 }
@@ -159,8 +210,39 @@ func (w *ResponseWriter) SetCookie(c *Cookie) error {
 	return w.header.addCookie(c)
 }
 
-// Dispatcher TODO
+// Dispatcher is responsible for writing a response received from the
+// ResponseWriter to the underlying http.ResponseWriter.
+//
+// The implementation of a custom Dispatcher should be thoroughly reviewed by
+// the security team to avoid introducing vulnerabilities.
 type Dispatcher interface {
+	// Write writes a Response to the http.ResponseWriter.
+	//
+	// It should return an error if the writing operation fails or if the
+	// provided Response should not be written to the http.ResponseWriter
 	Write(rw http.ResponseWriter, resp Response) error
+
+	// WriteJSON serialises and writes a JSON object to the http.ResponseWriter.
+	//
+	// This should return an error if the provided response is not a valid
+	// JSONResponse  or if writing the object to the http.ResponseWriter fails.
+	WriteJSON(rw http.ResponseWriter, resp JSONResponse) error
+
+	// ExecuteTemplate applies a parsed template to the provided data object
+	// and writes the output to the http.ResponseWriter.
+	//
+	// This should return an error if the provided Template response is not a
+	// valid template or if an error occurs executing or writing the template.
 	ExecuteTemplate(rw http.ResponseWriter, t Template, data interface{}) error
+
+	// Content-Type returns the Content-Type of the provided response if it is
+	// of a type supported by the Dispatcher and should return an error
+	// otherwise.
+	//
+	// Sending a response to the http.ResponseWriter without properly setting
+	// CT is error-prone and could introduce vulnerabilities. Therefore, this
+	// method should be used to set the Content-Type header before calling any
+	// of the writing methods in the Dispatcher. Writing should not proceed
+	// if ContentType returns an error.
+	ContentType(resp Response) (string, error)
 }


Test patch:
diff --git a/safehttp/default_dispatcher_test.go b/safehttp/default_dispatcher_test.go
new file mode 100644
index 0000000..7401e2d
--- /dev/null
+++ b/safehttp/default_dispatcher_test.go
@@ -0,0 +1,127 @@
+// 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 safehttp_test
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+	safetemplate "github.com/google/safehtml/template"
+	"html/template"
+	"math"
+	"net/http"
+	"strings"
+	"testing"
+)
+
+func TestDefaultDispatcherValidResponse(t *testing.T) {
+	tests := []struct {
+		name        string
+		write       func(w http.ResponseWriter) error
+		wantStatus  safehttp.StatusCode
+		wantHeaders map[string][]string
+		wantBody    string
+	}{
+		{
+			name: "Safe HTML Response",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				return d.Write(w, safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+		},
+		{
+			name: "Safe HTML Template Response",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				return d.ExecuteTemplate(w, safetemplate.Must(safetemplate.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+			},
+			wantBody: "<h1>This is an actual heading, though.</h1>",
+		},
+		{
+			name: "Valid JSON Response",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				data := struct {
+					Field string `json:"field"`
+				}{Field: "myField"}
+				return d.WriteJSON(w, safehttp.JSONResponse{Data: data})
+			},
+			wantBody: ")]}',\n{\"field\":\"myField\"}\n",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b := &strings.Builder{}
+			rw := newResponseRecorder(b)
+
+			err := tt.write(rw)
+
+			if err != nil {
+				t.Errorf("tt.write(rw): got error %v, want nil", err)
+			}
+
+			if gotBody := b.String(); tt.wantBody != gotBody {
+				t.Errorf("response body: got %q, want %q", gotBody, tt.wantBody)
+			}
+		})
+	}
+}
+
+func TestDefaultDispatcherInvalidResponse(t *testing.T) {
+	tests := []struct {
+		name  string
+		write func(w http.ResponseWriter) error
+		want  string
+	}{
+		{
+			name: "Unsafe HTML Response",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				return d.Write(w, "<h1>Hello World!</h1>")
+			},
+			want: "",
+		},
+		{
+			name: "Unsafe Template Response",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				return d.ExecuteTemplate(w, template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+			},
+			want: "",
+		},
+		{
+			name: "Invalid JSON Response",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				return d.WriteJSON(w, safehttp.JSONResponse{Data: math.Inf(1)})
+			},
+			want: ")]}',\n",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b := &strings.Builder{}
+			rw := newResponseRecorder(b)
+
+			if err := tt.write(rw); err == nil {
+				t.Error("tt.write(rw): got nil, want error")
+			}
+
+			if want, got := tt.want, b.String(); want != got {
+				t.Errorf("response body: got %q, want %q", got, want)
+			}
+		})
+	}
+}
diff --git a/safehttp/mux_test.go b/safehttp/mux_test.go
index 0d208e8..d0a9fd5 100644
--- a/safehttp/mux_test.go
+++ b/safehttp/mux_test.go
@@ -38,8 +38,10 @@ func TestMuxOneHandlerOneRequest(t *testing.T) {
 			name:       "Valid Request",
 			req:        httptest.NewRequest(safehttp.MethodGet, "http://foo.com/", nil),
 			wantStatus: safehttp.StatusOK,
-			wantHeader: map[string][]string{},
-			wantBody:   "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+			wantHeader: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 		{
 			name:       "Invalid Host",
@@ -107,9 +109,11 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 			hf: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World! GET</h1>"))
 			}),
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{},
-			wantBody:    "&lt;h1&gt;Hello World! GET&lt;/h1&gt;",
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+			},
+			wantBody: "&lt;h1&gt;Hello World! GET&lt;/h1&gt;",
 		},
 		{
 			name: "POST Handler",
@@ -117,9 +121,11 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 			hf: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World! POST</h1>"))
 			}),
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{},
-			wantBody:    "&lt;h1&gt;Hello World! POST&lt;/h1&gt;",
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+			},
+			wantBody: "&lt;h1&gt;Hello World! POST&lt;/h1&gt;",
 		},
 	}
 
@@ -265,9 +271,12 @@ func TestMuxInterceptors(t *testing.T) {
 				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
 				return mux
 			}(),
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{"Foo": {"bar"}},
-			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+				"Foo":          {"bar"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 		{
 			name: "Install Interrupting Interceptor",
@@ -303,9 +312,12 @@ func TestMuxInterceptors(t *testing.T) {
 
 				return mux
 			}(),
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{"Foo": {"bar"}},
-			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+				"Foo":          {"bar"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 		{
 			name: "Panicking interceptor recovered by ServeMux",
@@ -340,9 +352,12 @@ func TestMuxInterceptors(t *testing.T) {
 
 				return mux
 			}(),
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{"Foo": {"bar"}},
-			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Foo":          {"bar"},
+				"Content-Type": {"text/html; charset=utf-8"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 	}
 
@@ -419,18 +434,26 @@ func TestMuxInterceptorConfigs(t *testing.T) {
 		wantBody    string
 	}{
 		{
-			name:        "SetHeaderInterceptor with config",
-			config:      setHeaderConfig{name: "Foo", value: "Bar"},
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{"Foo": {"Bar"}, "Commit-Foo": {"Bar"}},
-			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+			name:       "SetHeaderInterceptor with config",
+			config:     setHeaderConfig{name: "Foo", value: "Bar"},
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+				"Commit-Foo":   {"Bar"},
+				"Foo":          {"Bar"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 		{
-			name:        "SetHeaderInterceptor with mismatching config",
-			config:      noInterceptorConfig{},
-			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{"Pizza": {"Hawaii"}, "Commit-Pizza": {"Hawaii"}},
-			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+			name:       "SetHeaderInterceptor with mismatching config",
+			config:     noInterceptorConfig{},
+			wantStatus: safehttp.StatusOK,
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+				"Pizza":        {"Hawaii"},
+				"Commit-Pizza": {"Hawaii"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 	}
 
@@ -539,12 +562,13 @@ func TestMuxDeterministicInterceptorOrder(t *testing.T) {
 		t.Errorf("rw.status: got %v want %v", rw.status, want)
 	}
 	wantHeaders := map[string][]string{
-		"Dessert":   {"tiramisu"},
-		"Pizza":     {"diavola"},
-		"Spaghetti": {"bolognese"},
-		"Commit1":   {"a"},
-		"Commit2":   {"b"},
-		"Commit3":   {"c"},
+		"Dessert":      {"tiramisu"},
+		"Pizza":        {"diavola"},
+		"Spaghetti":    {"bolognese"},
+		"Commit1":      {"a"},
+		"Commit2":      {"b"},
+		"Commit3":      {"c"},
+		"Content-Type": {"text/html; charset=utf-8"},
 	}
 	if diff := cmp.Diff(wantHeaders, map[string][]string(rw.header)); diff != "" {
 		t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
diff --git a/safehttp/recorder_dispatcher_test.go b/safehttp/recorder_dispatcher_test.go
index 771639b..3991ac9 100644
--- a/safehttp/recorder_dispatcher_test.go
+++ b/safehttp/recorder_dispatcher_test.go
@@ -15,6 +15,8 @@
 package safehttp_test
 
 import (
+	"encoding/json"
+	"errors"
 	"io"
 	"net/http"
 	"text/template"
@@ -29,6 +31,17 @@ import (
 
 type testDispatcher struct{}
 
+func (testDispatcher) ContentType(resp safehttp.Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case safehttp.JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", errors.New("not a safe response")
+	}
+}
+
 func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
@@ -39,6 +52,15 @@ func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) erro
 	}
 }
 
+func (testDispatcher) WriteJSON(rw http.ResponseWriter, resp safehttp.JSONResponse) error {
+	obj, err := json.Marshal(resp.Data)
+	if err != nil {
+		panic("invalid json")
+	}
+	_, err = rw.Write(obj)
+	return err
+}
+
 func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	switch x := t.(type) {
 	case *template.Template:
diff --git a/safehttp/response_writer_test.go b/safehttp/response_writer_test.go
index d8ee1cc..d99590d 100644
--- a/safehttp/response_writer_test.go
+++ b/safehttp/response_writer_test.go
@@ -66,6 +66,22 @@ func TestResponseWriterWriteTwicePanic(t *testing.T) {
 				w.Write(safehtml.HTMLEscaped("<h1>Escaped, so not really a heading</h1>"))
 			},
 		},
+		{
+			name: "Call WriteJSON twice",
+			write: func(w *safehttp.ResponseWriter) {
+				obj := struct{ Field string }{Field: "myField"}
+				w.WriteJSON(obj)
+				w.WriteJSON(obj)
+			},
+		},
+		{
+			name: "Call Write then WriteJSON",
+			write: func(w *safehttp.ResponseWriter) {
+				obj := struct{ Field string }{Field: "myField"}
+				w.Write(obj)
+				w.WriteJSON(obj)
+			},
+		},
 		{
 			name: "Call WriteTemplate twice",
 			write: func(w *safehttp.ResponseWriter) {
diff --git a/safehttp/safehttptest/recorder.go b/safehttp/safehttptest/recorder.go
index 894dc99..31a0538 100644
--- a/safehttp/safehttptest/recorder.go
+++ b/safehttp/safehttptest/recorder.go
@@ -15,6 +15,8 @@
 package safehttptest
 
 import (
+	"encoding/json"
+	"errors"
 	"html/template"
 	"io"
 	"net/http"
@@ -26,6 +28,17 @@ import (
 
 type testDispatcher struct{}
 
+func (testDispatcher) ContentType(resp safehttp.Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case safehttp.JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", errors.New("not a safe response")
+	}
+}
+
 func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
@@ -36,6 +49,15 @@ func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) erro
 	}
 }
 
+func (testDispatcher) WriteJSON(rw http.ResponseWriter, resp safehttp.JSONResponse) error {
+	obj, err := json.Marshal(resp.Data)
+	if err != nil {
+		panic("invalid json")
+	}
+	_, err = rw.Write(obj)
+	return err
+}
+
 func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	switch x := t.(type) {
 	case *template.Template:
diff --git a/tests/formparams/form_params_test.go b/tests/formparams/form_params_test.go
index 4f4c1c8..e677670 100644
--- a/tests/formparams/form_params_test.go
+++ b/tests/formparams/form_params_test.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 // Package formparams provides tests to inspect the behaviour of form
-// parameters parsing in HTTP requests
+// parameters parsing in HTTP requests.
 package formparams
 
 import (
diff --git a/tests/integration/header/header_test.go b/tests/integration/header/header_test.go
index d632a79..5c2b1ee 100644
--- a/tests/integration/header/header_test.go
+++ b/tests/integration/header/header_test.go
@@ -16,6 +16,9 @@ package header
 
 import (
 	"bufio"
+	"encoding/json"
+	"errors"
+	"github.com/google/safehtml/template"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -29,6 +32,17 @@ import (
 
 type testDispatcher struct{}
 
+func (testDispatcher) ContentType(resp safehttp.Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case safehttp.JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", errors.New("not a safe response")
+	}
+}
+
 func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
@@ -39,6 +53,15 @@ func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) erro
 	}
 }
 
+func (testDispatcher) WriteJSON(rw http.ResponseWriter, resp safehttp.JSONResponse) error {
+	obj, err := json.Marshal(resp.Data)
+	if err != nil {
+		panic("invalid json")
+	}
+	_, err = rw.Write(obj)
+	return err
+}
+
 func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	return nil
 }
diff --git a/tests/integration/hsts/hsts_test.go b/tests/integration/hsts/hsts_test.go
index 1eee741..ea79791 100644
--- a/tests/integration/hsts/hsts_test.go
+++ b/tests/integration/hsts/hsts_test.go
@@ -15,6 +15,9 @@
 package hsts_test
 
 import (
+	"encoding/json"
+	"errors"
+	"github.com/google/safehtml/template"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -29,6 +32,17 @@ import (
 
 type dispatcher struct{}
 
+func (dispatcher) ContentType(resp safehttp.Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case safehttp.JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", errors.New("not a safe response")
+	}
+}
+
 func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
@@ -39,6 +53,15 @@ func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	}
 }
 
+func (dispatcher) WriteJSON(rw http.ResponseWriter, resp safehttp.JSONResponse) error {
+	obj, err := json.Marshal(resp.Data)
+	if err != nil {
+		panic("invalid json")
+	}
+	_, err = rw.Write(obj)
+	return err
+}
+
 func (dispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	return nil
 }
@@ -86,6 +109,7 @@ func TestHSTSServeMuxInstall(t *testing.T) {
 	}
 
 	wantHeaders := map[string][]string{
+		"Content-Type":              {"text/html; charset=utf-8"},
 		"Strict-Transport-Security": {"max-age=63072000; includeSubDomains"},
 	}
 	if diff := cmp.Diff(wantHeaders, map[string][]string(rr.header)); diff != "" {
diff --git a/tests/integration/mux/mux_test.go b/tests/integration/mux/mux_test.go
new file mode 100644
index 0000000..4a96531
--- /dev/null
+++ b/tests/integration/mux/mux_test.go
@@ -0,0 +1,206 @@
+// 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 safemux_test
+
+import (
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+	safetemplate "github.com/google/safehtml/template"
+	"html/template"
+	"io"
+	"math"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+type responseRecorder struct {
+	headers http.Header
+	writer  io.Writer
+	status  safehttp.StatusCode
+}
+
+func newResponseRecorder(w io.Writer) *responseRecorder {
+	return &responseRecorder{headers: http.Header{}, writer: w}
+}
+
+func (r *responseRecorder) Header() http.Header {
+	return r.headers
+}
+
+func (r *responseRecorder) WriteHeader(statusCode int) {
+	r.status = safehttp.StatusCode(statusCode)
+}
+
+func (r *responseRecorder) Write(data []byte) (int, error) {
+	return r.writer.Write(data)
+}
+
+func TestMuxDefaultDispatcher(t *testing.T) {
+	tests := []struct {
+		name        string
+		mux         *safehttp.ServeMux
+		wantHeaders map[string][]string
+		wantBody    string
+	}{
+		{
+			name: "Safe HTML Response",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+
+				h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+				})
+				mux.Handle("/pizza", safehttp.MethodGet, h)
+				return mux
+			}(),
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+			},
+			wantBody: "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+		},
+		{
+			name: "Safe HTML Template Response",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+
+				h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.WriteTemplate(safetemplate.Must(safetemplate.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+				})
+				mux.Handle("/pizza", safehttp.MethodGet, h)
+				return mux
+			}(),
+			wantHeaders: map[string][]string{
+				"Content-Type": {"text/html; charset=utf-8"},
+			},
+			wantBody: "<h1>This is an actual heading, though.</h1>",
+		},
+		{
+			name: "Valid JSON Response",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+
+				h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					data := struct {
+						Field string `json:"field"`
+					}{Field: "myField"}
+					return w.WriteJSON(data)
+				})
+				mux.Handle("/pizza", safehttp.MethodGet, h)
+				return mux
+			}(),
+			wantHeaders: map[string][]string{
+				"Content-Type": {"application/json; charset=utf-8"},
+			},
+			wantBody: ")]}',\n{\"field\":\"myField\"}\n",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := httptest.NewRequest(safehttp.MethodGet, "http://foo.com/pizza", nil)
+			b := &strings.Builder{}
+			rw := newResponseRecorder(b)
+
+			tt.mux.ServeHTTP(rw, req)
+
+			if wantStatus := safehttp.StatusOK; rw.status != wantStatus {
+				t.Errorf("rw.status: got %v want %v", rw.status, wantStatus)
+			}
+
+			if diff := cmp.Diff(tt.wantHeaders, map[string][]string(rw.headers)); diff != "" {
+				t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+			}
+
+			if gotBody := b.String(); tt.wantBody != gotBody {
+				t.Errorf("response body: got %v, want %v", gotBody, tt.wantBody)
+			}
+		})
+	}
+}
+
+func TestMuxDefaultDispatcherUnsafeResponses(t *testing.T) {
+	tests := []struct {
+		name string
+		mux  *safehttp.ServeMux
+	}{
+		{
+			name: "Unsafe HTML Response",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+
+				h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.Write("<h1>Hello World!</h1>")
+				})
+				mux.Handle("/pizza", safehttp.MethodGet, h)
+				return mux
+			}(),
+		},
+		{
+			name: "Unsafe Template Response",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+
+				h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+				})
+				mux.Handle("/pizza", safehttp.MethodGet, h)
+				return mux
+			}(),
+		},
+		{
+			name: "Invalid JSON Response",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+
+				h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.WriteJSON(math.Inf(1))
+				})
+				mux.Handle("/pizza", safehttp.MethodGet, h)
+				return mux
+			}(),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// TODO: Unskip these test cases and combine them with the test
+			// cases from the previous test into a single table test after
+			// error-handling in the ResponseWriter has been fixed.
+			t.Skip()
+			req := httptest.NewRequest(safehttp.MethodGet, "http://foo.com/pizza", nil)
+			b := &strings.Builder{}
+			rw := newResponseRecorder(b)
+
+			tt.mux.ServeHTTP(rw, req)
+
+			if wantStatus := safehttp.StatusInternalServerError; rw.status != wantStatus {
+				t.Errorf("rw.status: got %v want %v", rw.status, wantStatus)
+			}
+
+			wantHeaders := map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			}
+			if diff := cmp.Diff(wantHeaders, map[string][]string(rw.headers)); diff != "" {
+				t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+			}
+
+			if wantBody, gotBody := "Internal Server Error\n", b.String(); wantBody != gotBody {
+				t.Errorf("response body: got %v, want %v", gotBody, wantBody)
+			}
+		})
+	}
+}
diff --git a/tests/integration/safehtml/safehtml_test.go b/tests/integration/safehtml/safehtml_test.go
index 475bc5d..0119400 100644
--- a/tests/integration/safehtml/safehtml_test.go
+++ b/tests/integration/safehtml/safehtml_test.go
@@ -15,6 +15,8 @@
 package safehtml_test
 
 import (
+	"encoding/json"
+	"errors"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -28,6 +30,17 @@ import (
 
 type testDispatcher struct{}
 
+func (testDispatcher) ContentType(resp safehttp.Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case safehttp.JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", errors.New("not a safe response")
+	}
+}
+
 func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
@@ -38,6 +51,15 @@ func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) erro
 	}
 }
 
+func (testDispatcher) WriteJSON(rw http.ResponseWriter, resp safehttp.JSONResponse) error {
+	obj, err := json.Marshal(resp.Data)
+	if err != nil {
+		panic("invalid json")
+	}
+	_, err = rw.Write(obj)
+	return err
+}
+
 func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	switch x := t.(type) {
 	case *template.Template:
diff --git a/tests/integration/staticheaders/staticheaders_test.go b/tests/integration/staticheaders/staticheaders_test.go
index 5b83a9b..d9b0368 100644
--- a/tests/integration/staticheaders/staticheaders_test.go
+++ b/tests/integration/staticheaders/staticheaders_test.go
@@ -15,6 +15,8 @@
 package staticheaders_test
 
 import (
+	"encoding/json"
+	"errors"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -30,6 +32,17 @@ import (
 
 type dispatcher struct{}
 
+func (dispatcher) ContentType(resp safehttp.Response) (string, error) {
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
+		return "text/html; charset=utf-8", nil
+	case safehttp.JSONResponse:
+		return "application/json; charset=utf-8", nil
+	default:
+		return "", errors.New("not a safe response")
+	}
+}
+
 func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
@@ -40,6 +53,15 @@ func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	}
 }
 
+func (dispatcher) WriteJSON(rw http.ResponseWriter, resp safehttp.JSONResponse) error {
+	obj, err := json.Marshal(resp.Data)
+	if err != nil {
+		panic("invalid json")
+	}
+	_, err = rw.Write(obj)
+	return err
+}
+
 func (dispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	switch x := t.(type) {
 	case *template.Template:
@@ -92,6 +114,7 @@ func TestServeMuxInstallStaticHeaders(t *testing.T) {
 	}
 
 	wantHeaders := map[string][]string{
+		"Content-Type":           {"text/html; charset=utf-8"},
 		"X-Content-Type-Options": {"nosniff"},
 		"X-Xss-Protection":       {"0"},
 	}
diff --git a/tests/queryparams/query_params_test.go b/tests/queryparams/query_params_test.go
index c74f2d9..7797b15 100644
--- a/tests/queryparams/query_params_test.go
+++ b/tests/queryparams/query_params_test.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 // Package queryparams provides tests to inspect the behaviour of query
-// parameters parsing in HTTP requests
+// parameters parsing in HTTP requests.
 package queryparams
 
 import (