Repository: google/go-safeweb (go)

Issue:
Add template nonce injection
The CSP and XSRF plugins currently put tokens in the request context.

* There needs to be a part of the plugins that can, later on, provide that nonce for a template to render.
* This should ideally work easily with different template implementations
* This will likely happen in the Commit phase of both plugins

Pull request description:
Implemented the Commit phase in the CSP and XSRF plugins
Fixes #76 

Extended the `safehttp.TemplateResponse` to support adding elements to a function map. This will be executed before the template is parsed. The CSP nonce and XSRF Token values will be added to this map in the `xsrf.Commit` and `csp.Commit` phases.

Patch:
diff --git a/safehttp/default_dispatcher.go b/safehttp/default_dispatcher.go
index ba028e1..a2366b3 100644
--- a/safehttp/default_dispatcher.go
+++ b/safehttp/default_dispatcher.go
@@ -60,17 +60,28 @@ func (DefaultDispatcher) WriteJSON(rw http.ResponseWriter, resp JSONResponse) er
 	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.
+// ExecuteTemplate applies the parsed template to the provided data object, if
+// the template is a safe HTML template, writing the output to the  http.
+// ResponseWriter. If the funcMap is non-nil, its elements override the
+// existing names to functions mappings in the template. An attempt to define a
+// new name to function mapping that is not already in the template will result
+// in a panic.
 //
 // 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 {
+func (DefaultDispatcher) ExecuteTemplate(rw http.ResponseWriter, resp TemplateResponse) error {
+	t := *resp.Template
 	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 fmt.Errorf("%T is not a safe template and it cannot be parsed and written", resp.Template)
 	}
-	return x.Execute(rw, data)
+	if len(resp.FuncMap) == 0 {
+		return x.Execute(rw, resp.Data)
+	}
+	cloned, err := x.Clone()
+	if err != nil {
+		return err
+	}
+	return cloned.Funcs(resp.FuncMap).Execute(rw, resp.Data)
 }
diff --git a/safehttp/plugins/csp/csp.go b/safehttp/plugins/csp/csp.go
index 3ca45c9..01d1bc4 100644
--- a/safehttp/plugins/csp/csp.go
+++ b/safehttp/plugins/csp/csp.go
@@ -215,7 +215,26 @@ func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 	return safehttp.NotWritten()
 }
 
-// Commit is a no-op, required to satisfy the safehttp.Interceptor interface.
+// Commit adds the nonce to the safehttp.TemplateResponse which is going to be
+// injected as the value of the nonce attribute in <script> and <link> tags. The
+// nonce is going to be unique for each safehttp.IncomingRequest.
 func (it Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	tmplResp, ok := resp.(safehttp.TemplateResponse)
+	if !ok {
+		return safehttp.NotWritten()
+	}
+
+	nonce, err := Nonce(r.Context())
+	if err != nil {
+		// The nonce should have been added in the Before stage and, if that is
+		// not the case, a server misconfiguration occured.
+		return w.WriteError(safehttp.StatusInternalServerError)
+	}
+
+	// TODO(maramihali@): Change the key when function names are exported by
+	// htmlinject
+	// TODO: What should happen if the CSPNonce is not present in the
+	// tr.FuncMap?
+	tmplResp.FuncMap["CSPNonce"] = func() string { return nonce }
 	return safehttp.NotWritten()
 }
diff --git a/safehttp/plugins/xsrf/xsrf.go b/safehttp/plugins/xsrf/xsrf.go
index a2abe64..cbf8482 100644
--- a/safehttp/plugins/xsrf/xsrf.go
+++ b/safehttp/plugins/xsrf/xsrf.go
@@ -94,7 +94,7 @@ func addCookieID(w *safehttp.ResponseWriter) (*safehttp.Cookie, error) {
 // In case of state changing requests (all except GET, HEAD and OPTIONS), the
 // interceptor checks for the presence of the XSRF token in the request body
 // (expected to have been injected) and validates it.
-func (i *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
+func (it *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
 	needsValidation := !statePreservingMethods[r.Method()]
 	cookieID, err := r.Cookie(cookieIDKey)
 	if err != nil {
@@ -128,17 +128,36 @@ func (i *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 			return w.WriteError(safehttp.StatusUnauthorized)
 		}
 
-		if ok := xsrftoken.Valid(tok, i.SecretAppKey, cookieID.Value(), actionID); !ok {
+		if ok := xsrftoken.Valid(tok, it.SecretAppKey, cookieID.Value(), actionID); !ok {
 			return w.WriteError(safehttp.StatusForbidden)
 		}
 	}
 
-	tok := xsrftoken.Generate(i.SecretAppKey, cookieID.Value(), actionID)
+	tok := xsrftoken.Generate(it.SecretAppKey, cookieID.Value(), actionID)
 	r.SetContext(context.WithValue(r.Context(), tokenCtxKey{}, tok))
 	return safehttp.NotWritten()
 }
 
-// Commit is a no-op, required to satisfy the safehttp.Interceptor interface.
-func (i *Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+// Commit adds the XSRF token corresponding to the safehttp.TemplateResponse
+// with key "XSRFToken". The token corresponds to the user information found in
+// the request.
+func (it *Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	tmplResp, ok := resp.(safehttp.TemplateResponse)
+	if !ok {
+		return safehttp.NotWritten()
+	}
+
+	tok, err := Token(r)
+	if err != nil {
+		// The token should have been added in the Before stage and if that is
+		// not the case, a server misconfiguration occured.
+		return w.WriteError(safehttp.StatusInternalServerError)
+	}
+
+	// TODO(maramihali@): Change the key when function names are exported by
+	// htmlinject
+	// TODO: what should happen if the XSRFToken key is not present in the
+	// tr.FuncMap?
+	tmplResp.FuncMap["XSRFToken"] = func() string { return tok }
 	return safehttp.NotWritten()
 }
diff --git a/safehttp/response.go b/safehttp/response.go
index c2e4980..d3ef1df 100644
--- a/safehttp/response.go
+++ b/safehttp/response.go
@@ -38,12 +38,12 @@ type Template interface {
 	Execute(wr io.Writer, data interface{}) error
 }
 
-// TemplateResponse bundles a Template with its data to be passed together to the
-// commit phase. This will be passed when the commit phase is initiated from
-// ResponseWriter.WriteTemplate.
+// TemplateResponse bundles a Template with its data and names to function
+// mappings to be passed together to the commit phase.
 type TemplateResponse struct {
 	Template *Template
 	Data     *interface{}
+	FuncMap  map[string]interface{}
 }
 
 // NoContentResponse is sent to the commit phase when it's initiated from
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index 0669228..c2b493f 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -132,7 +132,8 @@ func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 	if w.written {
 		panic("ResponseWriter was already written to")
 	}
-	w.handler.commitPhase(w, TemplateResponse{Template: &t, Data: &data})
+	resp := TemplateResponse{Template: &t, Data: &data, FuncMap: map[string]interface{}{}}
+	w.handler.commitPhase(w, resp)
 	if w.written {
 		return Result{}
 	}
@@ -143,7 +144,7 @@ func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 	}
 	w.rw.Header().Set("Content-Type", ct)
 	w.rw.WriteHeader(int(StatusOK))
-	if err := w.d.ExecuteTemplate(w.rw, t, data); err != nil {
+	if err := w.d.ExecuteTemplate(w.rw, resp); err != nil {
 		panic(err)
 	}
 	return Result{}
@@ -224,16 +225,19 @@ type Dispatcher interface {
 
 	// WriteJSON serialises and writes a JSON object to the http.ResponseWriter.
 	//
-	// This should return an error if the provided response is not a valid
+	// This should return an error if the provided response is not a safe
 	// 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.
+	// ExecuteTemplate applies the response parsed template to its data object
+	// and writes the output to the http.ResponseWriter. If the response funcMap
+	// is non-nil, the Dispatcher will try to update the names to functions
+	// mappings of the template.
 	//
-	// 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
+	// This should return an error if the template contained by the
+	// TemplateResponse is not a safe template or if an error occurs executing
+	// or writing the template.
+	ExecuteTemplate(rw http.ResponseWriter, resp TemplateResponse) 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


Test patch:
diff --git a/safehttp/default_dispatcher_test.go b/safehttp/default_dispatcher_test.go
index d2bc67c..ed58b41 100644
--- a/safehttp/default_dispatcher_test.go
+++ b/safehttp/default_dispatcher_test.go
@@ -46,10 +46,67 @@ func TestDefaultDispatcherValidResponse(t *testing.T) {
 			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.")
+				t := safehttp.Template(safetemplate.
+					Must(safetemplate.New("name").
+						Parse("<h1>{{ . }}</h1>")))
+				var data interface{}
+				data = "This is an actual heading, though."
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+				}
+				return d.ExecuteTemplate(w, resp)
 			},
 			wantBody: "<h1>This is an actual heading, though.</h1>",
 		},
+		{
+			name: "Safe HTML Template Response with Token",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				defaultFunc := func() string { panic("this function should never be called") }
+				t := safehttp.Template(safetemplate.
+					Must(safetemplate.New("name").
+						Funcs(map[string]interface{}{"Token": defaultFunc}).
+						Parse(`<form><input type="hidden" name="token" value="{{Token}}">{{.}}</form>`)))
+				var data interface{}
+				data = "Content"
+				fm := map[string]interface{}{
+					"Token": func() string { return "Token-secret" },
+				}
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+					FuncMap:  fm,
+				}
+
+				return d.ExecuteTemplate(w, resp)
+			},
+			wantBody: `<form><input type="hidden" name="token" value="Token-secret">Content</form>`,
+		},
+		{
+			name: "Safe HTML Template Response with  Nonce",
+			write: func(w http.ResponseWriter) error {
+				d := &safehttp.DefaultDispatcher{}
+				defaultFunc := func() string { panic("this function should never be called") }
+				t := safehttp.Template(safetemplate.
+					Must(safetemplate.New("name").
+						Funcs(map[string]interface{}{"Nonce": defaultFunc}).
+						Parse(`<script nonce="{{Nonce}}" type="application/javascript">alert("script")</script><h1>{{.}}</h1>`)))
+				var data interface{}
+				data = "Content"
+				fm := map[string]interface{}{
+					"Nonce": func() string { return "Nonce-secret" },
+				}
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+					FuncMap:  fm,
+				}
+
+				return d.ExecuteTemplate(w, resp)
+			},
+			wantBody: `<script nonce="Nonce-secret" type="application/javascript">alert("script")</script><h1>Content</h1>`,
+		},
 		{
 			name: "Valid JSON Response",
 			write: func(w http.ResponseWriter) error {
@@ -98,7 +155,16 @@ func TestDefaultDispatcherInvalidResponse(t *testing.T) {
 			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.")
+				t := safehttp.Template(template.
+					Must(template.New("name").
+						Parse("<h1>{{ . }}</h1>")))
+				var data interface{}
+				data = "This is an actual heading, though."
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+				}
+				return d.ExecuteTemplate(w, resp)
 			},
 			want: "",
 		},
diff --git a/safehttp/plugins/csp/csp_test.go b/safehttp/plugins/csp/csp_test.go
index f743e1e..703dc5d 100644
--- a/safehttp/plugins/csp/csp_test.go
+++ b/safehttp/plugins/csp/csp_test.go
@@ -199,6 +199,10 @@ func (errorReader) Read(b []byte) (int, error) {
 
 func TestPanicWhileGeneratingNonce(t *testing.T) {
 	randReader = errorReader{}
+	defer func() {
+		// TODO: avoid replacing the randReader per individual test cases
+		randReader = endlessAReader{}
+	}()
 	defer func() {
 		if r := recover(); r == nil {
 			t.Error("generateNonce() expected panic")
@@ -230,3 +234,88 @@ func TestNonceEmptyContext(t *testing.T) {
 		t.Errorf("Nonce(ctx) got nonce: %v want: %v", n, want)
 	}
 }
+
+func TestCommitNonce(t *testing.T) {
+	rec := safehttptest.NewResponseRecorder()
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+	req.SetContext(context.WithValue(req.Context(), ctxKey{}, "pizza"))
+
+	it := Interceptor{}
+	tr := safehttp.TemplateResponse{FuncMap: map[string]interface{}{}}
+	it.Commit(rec.ResponseWriter, req, tr, nil)
+
+	nonce, ok := tr.FuncMap["CSPNonce"]
+	if !ok {
+		t.Fatal(`tr.FuncMap["CSPNonce"] not found`)
+	}
+
+	fn, ok := nonce.(func() string)
+	if !ok {
+		t.Fatalf(`tr.FuncMap["CSPNonce"]: got %T, want "func() string"`, fn)
+	}
+	if got, want := fn(), "pizza"; want != got {
+		t.Errorf(`tr.FuncMap["CSPNonce"](): got %q, want %q`, got, want)
+	}
+
+	if got, want := rec.Status(), safehttp.StatusOK; want != got {
+		t.Errorf("rec.Status(): got %v, want %v", got, want)
+	}
+
+	if diff := cmp.Diff(map[string][]string{}, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+	}
+
+	if got, want := "", rec.Body(); got != want {
+		t.Errorf("rec.Body(): got %q want %q", got, want)
+	}
+}
+
+func TestCommitMissingNonce(t *testing.T) {
+	rec := safehttptest.NewResponseRecorder()
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+	req.SetContext(context.Background())
+
+	it := Interceptor{}
+	tr := safehttp.TemplateResponse{FuncMap: map[string]interface{}{}}
+	it.Commit(rec.ResponseWriter, req, tr, nil)
+
+	wantFuncMap := map[string]interface{}{}
+	if diff := cmp.Diff(wantFuncMap, tr.FuncMap); diff != "" {
+		t.Errorf("tr.FuncMap: mismatch (-want +got):\n%s", diff)
+	}
+
+	if got, want := rec.Status(), safehttp.StatusInternalServerError; got != want {
+		t.Errorf("rec.Status(): got %v, want %v", got, want)
+	}
+	wantHeaders := map[string][]string{
+		"Content-Type":           {"text/plain; charset=utf-8"},
+		"X-Content-Type-Options": {"nosniff"},
+	}
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+	}
+	if got, want := rec.Body(), "Internal Server Error\n"; got != want {
+		t.Errorf("rec.Body(): got %q want %q", got, want)
+	}
+}
+
+func TestCommitNotTemplateResponse(t *testing.T) {
+	rec := safehttptest.NewResponseRecorder()
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+
+	it := Interceptor{}
+	it.Commit(rec.ResponseWriter, req, safehttp.NoContentResponse{}, nil)
+
+	if got, want := rec.Status(), safehttp.StatusOK; want != got {
+		t.Errorf("rec.Status(): got %v, want %v", got, want)
+	}
+
+	if diff := cmp.Diff(map[string][]string{}, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+	}
+
+	if got, want := rec.Body(), ""; got != want {
+		t.Errorf("rec.Body(): got %q want %q", got, want)
+	}
+
+}
diff --git a/safehttp/plugins/xsrf/xsrf_test.go b/safehttp/plugins/xsrf/xsrf_test.go
index 866c599..c6d1d1c 100644
--- a/safehttp/plugins/xsrf/xsrf_test.go
+++ b/safehttp/plugins/xsrf/xsrf_test.go
@@ -109,7 +109,7 @@ func TestTokenMultipart(t *testing.T) {
 				t.Errorf("rec.Status(): got %v, want %v", got, test.wantStatus)
 			}
 			if diff := cmp.Diff(test.wantHeader, map[string][]string(rec.Header())); diff != "" {
-				t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+				t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
 			}
 			if got := rec.Body(); got != test.wantBody {
 				t.Errorf("rec.Body(): got %q want %q", got, test.wantBody)
@@ -135,7 +135,7 @@ func TestMalformedForm(t *testing.T) {
 		"X-Content-Type-Options": {"nosniff"},
 	}
 	if diff := cmp.Diff(wantHeaders, map[string][]string(rec.Header())); diff != "" {
-		t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
 	}
 	if want, got := "Bad Request\n", rec.Body(); got != want {
 		t.Errorf("rec.Body(): got %q want %q", got, want)
@@ -208,7 +208,7 @@ func TestMissingTokenInBody(t *testing.T) {
 			"X-Content-Type-Options": {"nosniff"},
 		}
 		if diff := cmp.Diff(wantHeaders, map[string][]string(rec.Header())); diff != "" {
-			t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+			t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
 		}
 		if want, got := "Unauthorized\n", rec.Body(); got != want {
 			t.Errorf("rec.Body(): got %q want %q", got, want)
@@ -309,9 +309,94 @@ func TestMissingCookiePostRequest(t *testing.T) {
 		"X-Content-Type-Options": {"nosniff"},
 	}
 	if diff := cmp.Diff(wantHeaders, map[string][]string(rec.Header())); diff != "" {
-		t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
 	}
 	if want, got := "Forbidden\n", rec.Body(); got != want {
 		t.Errorf("rec.Body(): got %q want %q", got, want)
 	}
 }
+
+func TestCommitToken(t *testing.T) {
+	rec := safehttptest.NewResponseRecorder()
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+	req.SetContext(context.WithValue(req.Context(), tokenCtxKey{}, "pizza"))
+
+	i := Interceptor{SecretAppKey: "testSecretAppKey"}
+	tr := safehttp.TemplateResponse{FuncMap: map[string]interface{}{}}
+	i.Commit(rec.ResponseWriter, req, tr, nil)
+
+	tok, ok := tr.FuncMap["XSRFToken"]
+	if !ok {
+		t.Fatal(`tr.FuncMap["XSRFToken"] not found`)
+	}
+
+	fn, ok := tok.(func() string)
+	if !ok {
+		t.Fatalf(`tr.FuncMap["XSRFToken"]: got %T, want "func() string"`, fn)
+	}
+	if want, got := "pizza", fn(); want != got {
+		t.Errorf(`tr.FuncMap["XSRFToken"](): got %q, want %q`, got, want)
+	}
+
+	if want, got := safehttp.StatusOK, rec.Status(); want != got {
+		t.Errorf("rec.Status(): got %v, want %v", got, want)
+	}
+	if diff := cmp.Diff(map[string][]string{}, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+	}
+
+	if want, got := "", rec.Body(); got != want {
+		t.Errorf("rec.Body(): got %q want %q", got, want)
+	}
+}
+
+func TestCommitMissingToken(t *testing.T) {
+	rec := safehttptest.NewResponseRecorder()
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+	req.SetContext(context.Background())
+
+	i := Interceptor{SecretAppKey: "testSecretAppKey"}
+	tr := safehttp.TemplateResponse{FuncMap: map[string]interface{}{}}
+	i.Commit(rec.ResponseWriter, req, tr, nil)
+
+	wantFuncMap := map[string]interface{}{}
+	if diff := cmp.Diff(wantFuncMap, tr.FuncMap); diff != "" {
+		t.Errorf("tr.FuncMap: mismatch (-want +got):\n%s", diff)
+	}
+
+	if want, got := safehttp.StatusInternalServerError, rec.Status(); got != want {
+		t.Errorf("rec.Status(): got %v, want %v", got, want)
+	}
+	wantHeaders := map[string][]string{
+		"Content-Type":           {"text/plain; charset=utf-8"},
+		"X-Content-Type-Options": {"nosniff"},
+	}
+
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+	}
+
+	if want, got := "Internal Server Error\n", rec.Body(); got != want {
+		t.Errorf("rec.Body(): got %q want %q", got, want)
+	}
+}
+
+func TestCommitNotTemplateResponse(t *testing.T) {
+	rec := safehttptest.NewResponseRecorder()
+	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
+
+	i := Interceptor{SecretAppKey: "testSecretAppKey"}
+	i.Commit(rec.ResponseWriter, req, safehttp.NoContentResponse{}, nil)
+
+	if want, got := safehttp.StatusOK, rec.Status(); want != got {
+		t.Errorf("rec.Status(): got %v, want %v", got, want)
+	}
+
+	if diff := cmp.Diff(map[string][]string{}, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+	}
+
+	if want, got := "", rec.Body(); got != want {
+		t.Errorf("rec.Body(): got %q want %q", got, want)
+	}
+}
diff --git a/tests/integration/csp/csp_test.go b/tests/integration/csp/csp_test.go
new file mode 100644
index 0000000..6e17497
--- /dev/null
+++ b/tests/integration/csp/csp_test.go
@@ -0,0 +1,85 @@
+// 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 csp_test
+
+import (
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+	safetemplate "github.com/google/safehtml/template"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestServeMuxInstallCSP(t *testing.T) {
+	mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+	it := csp.Default("")
+	mux.Install(&it)
+
+	var nonce string
+	var err error
+	handler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+		fns := map[string]interface{}{
+			"CSPNonce": func() string { return "WrongNonce" },
+		}
+		// These are not used in the handler, just recorded to test whether the
+		// handler can retrieve the CSP nonce (e.g. for when the CSP plugin is
+		// installed, but auto-injection isn't yet supported and has to be done
+		// manually by the handler).
+		nonce, err = csp.Nonce(r.Context())
+		t := safetemplate.Must(safetemplate.New("name").Funcs(fns).Parse(`<script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script><h1>{{.}}</h1>`))
+
+		return w.WriteTemplate(t, "Content")
+	})
+	mux.Handle("/bar", safehttp.MethodGet, handler)
+
+	b := strings.Builder{}
+	rr := safehttptest.NewTestResponseWriter(&b)
+
+	req := httptest.NewRequest(safehttp.MethodGet, "https://foo.com/bar", nil)
+
+	mux.ServeHTTP(rr, req)
+
+	if err != nil {
+		t.Fatalf("csp.Nonce: got error %v", err)
+	}
+
+	if nonce == "" {
+		t.Fatalf("csp.Nonce: got %q, want non-empty", nonce)
+	}
+
+	if want, got := rr.Status(), safehttp.StatusOK; got != want {
+		t.Errorf("rr.Status() got: %v want: %v", got, want)
+	}
+
+	wantHeaders := map[string][]string{
+		"Content-Type": {"text/html; charset=utf-8"},
+		"Content-Security-Policy": {
+			"object-src 'none'; script-src 'unsafe-inline' 'nonce-" + nonce + "' 'strict-dynamic' https: http:; base-uri 'none'",
+			"frame-ancestors 'self'"},
+	}
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rr.Header())); diff != "" {
+		t.Errorf("rr.Header(): mismatch (-want +got):\n%s", diff)
+	}
+
+	wantBody := `<script nonce="` + nonce +
+		`" type="application/javascript">alert("script")</script><h1>Content</h1>`
+	if gotBody := b.String(); gotBody != wantBody {
+		t.Errorf("response body: got %q, want nonce %q", gotBody, wantBody)
+	}
+
+}
diff --git a/tests/integration/mux/mux_test.go b/tests/integration/mux/mux_test.go
index 591aff7..23a19c6 100644
--- a/tests/integration/mux/mux_test.go
+++ b/tests/integration/mux/mux_test.go
@@ -56,7 +56,9 @@ func TestMuxDefaultDispatcher(t *testing.T) {
 				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.")
+					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
@@ -132,7 +134,9 @@ func TestMuxDefaultDispatcherUnsafeResponses(t *testing.T) {
 				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.")
+					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
diff --git a/tests/integration/xsrf/xsrf_test.go b/tests/integration/xsrf/xsrf_test.go
new file mode 100644
index 0000000..6403050
--- /dev/null
+++ b/tests/integration/xsrf/xsrf_test.go
@@ -0,0 +1,74 @@
+// 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 xsrf_test
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+	safetemplate "github.com/google/safehtml/template"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestServeMuxInstallXSRF(t *testing.T) {
+	mux := safehttp.NewServeMux(safehttp.DefaultDispatcher{}, "foo.com")
+	it := xsrf.Interceptor{SecretAppKey: "testSecretAppKey"}
+	mux.Install(&it)
+
+	var token string
+	var err error
+	handler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+		fns := map[string]interface{}{
+			"XSRFToken": func() string { return "WrongToken" },
+		}
+		// These are not used in the handler, just recorded to test whether the
+		// handler can retrieve the XSRF token (e.g. for when the XSRF plugin is
+		// installed, but auto-injection isn't yet supported and has to be done
+		// manually by the handler).
+		token, err = xsrf.Token(r)
+		t := safetemplate.Must(safetemplate.New("name").Funcs(fns).Parse(`<form><input type="hidden" name="token" value="{{XSRFToken}}">{{.}}</form>`))
+
+		return w.WriteTemplate(t, "Content")
+	})
+	mux.Handle("/bar", safehttp.MethodGet, handler)
+
+	b := strings.Builder{}
+	rr := safehttptest.NewTestResponseWriter(&b)
+
+	req := httptest.NewRequest(safehttp.MethodGet, "https://foo.com/bar", nil)
+
+	mux.ServeHTTP(rr, req)
+
+	if err != nil {
+		t.Fatalf("xsrf.Token: got error %v", err)
+	}
+
+	if token == "" {
+		t.Fatalf("csp.Nonce: got %q, want non-empty", token)
+	}
+
+	if got, want := rr.Status(), safehttp.StatusOK; got != want {
+		t.Errorf("rr.Status() got: %v want: %v", got, want)
+	}
+
+	wantBody := `<form><input type="hidden" name="token" value="` +
+		token + `">Content</form>`
+	if gotBody := b.String(); gotBody != wantBody {
+		t.Errorf("response body: got %q, want token %q", gotBody, wantBody)
+	}
+
+}