Repository: google/go-safeweb (go)

Issue:
Prevent double-writing to the ResponseWriter
This is one of the most critical security features.

Double-writing should likely result in panic.

Pull request description:
Make sure that ResponseWriter is only written to once, otherwise panic.
Fixes #97 

To be able to store the state whether or not the `ResponseWriter` was written to or not the signature of `Handler` and `Interceptor.Before` had to change to accept a `*ResponseWriter` instead of a `ResponseWriter`. This needed to be changed in **a lot** of places. 

Patch:
diff --git a/safehttp/handler.go b/safehttp/handler.go
index 4a30a24..adbf2bf 100644
--- a/safehttp/handler.go
+++ b/safehttp/handler.go
@@ -15,17 +15,17 @@
 package safehttp
 
 // HandleFunc TODO
-type HandleFunc func(ResponseWriter, *IncomingRequest) Result
+type HandleFunc func(*ResponseWriter, *IncomingRequest) Result
 
 // Handler TODO
 type Handler interface {
-	ServeHTTP(ResponseWriter, *IncomingRequest) Result
+	ServeHTTP(*ResponseWriter, *IncomingRequest) Result
 }
 
 // HandlerFunc TODO
-type HandlerFunc func(ResponseWriter, *IncomingRequest) Result
+type HandlerFunc func(*ResponseWriter, *IncomingRequest) Result
 
 // ServeHTTP calls f(w, r).
-func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *IncomingRequest) Result {
+func (f HandlerFunc) ServeHTTP(w *ResponseWriter, r *IncomingRequest) Result {
 	return f(w, r)
 }
diff --git a/safehttp/interceptor.go b/safehttp/interceptor.go
index 09b5731..af164f1 100644
--- a/safehttp/interceptor.go
+++ b/safehttp/interceptor.go
@@ -22,5 +22,5 @@ type Interceptor interface {
 	// response is written to the ResponseWriter, then the remaining
 	// interceptors and the handler won't execute. If Before panics, it will be
 	// recovered and the ServeMux will respond with 500 Internal Server Error.
-	Before(w ResponseWriter, r *IncomingRequest) Result
+	Before(*ResponseWriter, *IncomingRequest) Result
 }
diff --git a/safehttp/mux.go b/safehttp/mux.go
index 19c5a0d..7dd2435 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -178,7 +178,8 @@ func (m methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}()
 
 	for _, intercep := range m.muxInterceps {
-		if res := intercep.Before(rw, ir); res.written {
+		intercep.Before(rw, ir)
+		if rw.written {
 			return
 		}
 	}
diff --git a/safehttp/plugins/csp/csp.go b/safehttp/plugins/csp/csp.go
index 541d076..988ba6b 100644
--- a/safehttp/plugins/csp/csp.go
+++ b/safehttp/plugins/csp/csp.go
@@ -172,7 +172,7 @@ func Default(reportURI string) Interceptor {
 
 // Before claims and sets the Content-Security-Policy header and the
 // Content-Security-Policy-Report-Only header.
-func (it Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 	nonce := generateNonce()
 	r.SetContext(context.WithValue(r.Context(), ctxKey{}, nonce))
 
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index 91b9008..b71e8a7 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -65,7 +65,7 @@ func Default() Interceptor {
 // The function redirects HTTP requests to HTTPS. When HTTPS traffic
 // is received the Strict-Transport-Security header is applied to the
 // response.
-func (it Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 	if it.MaxAge < 0 {
 		return w.ServerError(safehttp.StatusInternalServerError)
 	}
diff --git a/safehttp/plugins/staticheaders/staticheaders.go b/safehttp/plugins/staticheaders/staticheaders.go
index d9c1423..3853a59 100644
--- a/safehttp/plugins/staticheaders/staticheaders.go
+++ b/safehttp/plugins/staticheaders/staticheaders.go
@@ -24,7 +24,7 @@ type Plugin struct{}
 // Before claims and sets the following headers:
 //  - X-Content-Type-Options: nosniff
 //  - X-XSS-Protection: 0
-func (Plugin) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+func (Plugin) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 	h := w.Header()
 	setXCTO, err := h.Claim("X-Content-Type-Options")
 	if err != nil {
diff --git a/safehttp/plugins/xsrf/xsrf.go b/safehttp/plugins/xsrf/xsrf.go
index 7fc34dd..78b4a25 100644
--- a/safehttp/plugins/xsrf/xsrf.go
+++ b/safehttp/plugins/xsrf/xsrf.go
@@ -47,7 +47,7 @@ type Interceptor struct {
 // the handler to ensure it is not part of the Cross Site Request
 // Forgery. It checks for the presence of an xsrf-token in the request body and
 // validates it based on the userID associated with the request.
-func (p *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+func (p *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 	userID, err := p.Identifier.UserID(r)
 	if err != nil {
 		return w.ClientError(safehttp.StatusUnauthorized)
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index aec52aa..67308db 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -27,13 +27,14 @@ type ResponseWriter struct {
 	// easily overwrite the struct bypassing all our safety guarantees.
 	header       Header
 	muxInterceps map[string]Interceptor
+	written      bool
 }
 
 // NewResponseWriter creates a ResponseWriter from a safehttp.Dispatcher, an
 // http.ResponseWriter and a list of interceptors associated with a ServeMux.
-func NewResponseWriter(d Dispatcher, rw http.ResponseWriter, muxInterceps map[string]Interceptor) ResponseWriter {
+func NewResponseWriter(d Dispatcher, rw http.ResponseWriter, muxInterceps map[string]Interceptor) *ResponseWriter {
 	header := newHeader(rw.Header())
-	return ResponseWriter{
+	return &ResponseWriter{
 		d:            d,
 		rw:           rw,
 		header:       header,
@@ -51,52 +52,67 @@ func (w *ResponseWriter) Interceptor(key string) Interceptor {
 }
 
 // Result TODO
-type Result struct {
-	written bool
-}
+type Result struct{}
 
 // Write TODO
 func (w *ResponseWriter) Write(resp Response) Result {
+	w.markWritten()
 	if err := w.d.Write(w.rw, resp); err != nil {
 		panic("error")
 	}
-	return Result{written: true}
+	return Result{}
 }
 
 // WriteTemplate TODO
 func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
+	w.markWritten()
 	if err := w.d.ExecuteTemplate(w.rw, t, data); err != nil {
 		panic("error")
 	}
-	return Result{written: true}
+	return Result{}
 }
 
 // ClientError TODO
 func (w *ResponseWriter) ClientError(code StatusCode) Result {
+	w.markWritten()
 	if code < 400 || code >= 500 {
 		// TODO(@mihalimara22): Replace this when we decide how to handle this case
 		panic("wrong method called")
 	}
 	http.Error(w.rw, http.StatusText(int(code)), int(code))
-	return Result{written: true}
+	return Result{}
 }
 
 // ServerError TODO
 func (w *ResponseWriter) ServerError(code StatusCode) Result {
+	w.markWritten()
 	if code < 500 || code >= 600 {
 		// TODO(@mattiasgrenfeldt, @mihalimara22, @kele, @empijei): Decide how it should
 		// be communicated to the user of the framework that they've called the wrong
 		// method.
-		return Result{written: true}
+		panic("wrong method called")
 	}
 	http.Error(w.rw, http.StatusText(int(code)), int(code))
-	return Result{written: true}
+	return Result{}
 }
 
 // Redirect responds with a redirect to a given url, using code as the status code.
 func (w *ResponseWriter) Redirect(r *IncomingRequest, url string, code StatusCode) Result {
+	w.markWritten()
+	if code < 300 || code >= 400 {
+		panic("wrong method called")
+	}
 	http.Redirect(w.rw, r.req, url, int(code))
-	return Result{written: true}
+	return Result{}
+}
+
+// markWritten ensures that the ResponseWriter is only written to once by panicking
+// if it is written more than once.
+func (w *ResponseWriter) markWritten() {
+	if w.written {
+		panic("ResponseWriter was already written to")
+	}
+	w.written = true
 }
 
 // Header returns the collection of headers that will be set


Test patch:
diff --git a/safehttp/form_test.go b/safehttp/form_test.go
index 003328f..c9c2017 100644
--- a/safehttp/form_test.go
+++ b/safehttp/form_test.go
@@ -61,7 +61,7 @@ func TestFormValidInt(t *testing.T) {
 
 	for _, test := range tests {
 		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 				var err error
@@ -150,7 +150,7 @@ func TestFormInvalidInt(t *testing.T) {
 	for _, test := range tests {
 		for _, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -221,7 +221,7 @@ func TestFormValidUint(t *testing.T) {
 
 	for _, test := range tests {
 		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 				var err error
@@ -308,7 +308,7 @@ func TestFormInvalidUint(t *testing.T) {
 	for _, test := range tests {
 		for _, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -390,7 +390,7 @@ func TestFormValidString(t *testing.T) {
 	for _, test := range tests {
 		for idx, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -473,7 +473,7 @@ func TestFormValidFloat64(t *testing.T) {
 	for _, test := range tests {
 		for idx, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -561,7 +561,7 @@ func TestFormInvalidFloat64(t *testing.T) {
 	for _, test := range tests {
 		for _, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -653,7 +653,7 @@ func TestFormValidBool(t *testing.T) {
 	for _, test := range tests {
 		for idx, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -735,7 +735,7 @@ func TestFormInvalidBool(t *testing.T) {
 	for _, test := range tests {
 		for _, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -833,7 +833,7 @@ func TestFormInvalidSlice(t *testing.T) {
 	for _, test := range tests {
 		for _, req := range test.reqs {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -908,7 +908,7 @@ func TestFormErrorHandling(t *testing.T) {
 
 	for _, tc := range tests {
 		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 				var err error
@@ -1017,7 +1017,7 @@ func TestFormValidSlicePost(t *testing.T) {
 
 	for _, tc := range tests {
 		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			form, err := ir.PostForm()
 			if err != nil {
 				t.Fatalf(`ir.PostForm: got err %v, want nil`, err)
@@ -1119,7 +1119,7 @@ func TestFormValidSliceMultipart(t *testing.T) {
 
 	for _, tc := range tests {
 		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			form, err := ir.MultipartForm(32 << 20)
 			if err != nil {
 				t.Fatalf(`ir.MultipartForm: got err %v, want nil`, err)
diff --git a/safehttp/mux_test.go b/safehttp/mux_test.go
index aee83b7..4fd01ff 100644
--- a/safehttp/mux_test.go
+++ b/safehttp/mux_test.go
@@ -66,7 +66,7 @@ func TestMuxOneHandlerOneRequest(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
 
-			h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+			h := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 			})
 			mux.Handle("/", safehttp.MethodGet, h)
@@ -103,7 +103,7 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 		{
 			name: "GET Handler",
 			req:  httptest.NewRequest("GET", "http://foo.com/bar", nil),
-			hf: safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+			hf: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World! GET</h1>"))
 			}),
 			wantStatus:  200,
@@ -113,7 +113,7 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 		{
 			name: "POST Handler",
 			req:  httptest.NewRequest("POST", "http://foo.com/bar", nil),
-			hf: safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+			hf: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World! POST</h1>"))
 			}),
 			wantStatus:  200,
@@ -149,7 +149,7 @@ func TestMuxHandleSameMethodTwice(t *testing.T) {
 	d := testDispatcher{}
 	mux := safehttp.NewServeMux(d, "foo.com")
 
-	registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+	registeredHandler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 	})
 	mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
@@ -168,7 +168,7 @@ type setHeaderInterceptor struct {
 	value string
 }
 
-func (p setHeaderInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+func (p setHeaderInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
 	if err := w.Header().Set(p.name, p.value); err != nil {
 		return w.ServerError(safehttp.StatusInternalServerError)
 	}
@@ -177,7 +177,7 @@ func (p setHeaderInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.Inco
 
 type internalErrorInterceptor struct{}
 
-func (internalErrorInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+func (internalErrorInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
 	return w.ServerError(safehttp.StatusInternalServerError)
 }
 
@@ -186,7 +186,7 @@ type claimHeaderInterceptor struct {
 	setValue      func([]string)
 }
 
-func (p *claimHeaderInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+func (p *claimHeaderInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
 	f, err := w.Header().Claim(p.headerToClaim)
 	if err != nil {
 		return w.ServerError(safehttp.StatusInternalServerError)
@@ -201,7 +201,7 @@ func (p *claimHeaderInterceptor) SetHeader(value string) {
 
 type panickingInterceptor struct{}
 
-func (panickingInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+func (panickingInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
 	panic("bad")
 }
 
@@ -222,7 +222,7 @@ func TestMuxInterceptors(t *testing.T) {
 					value: "bar",
 				})
 
-				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+				registeredHandler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 				})
 				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
@@ -237,7 +237,7 @@ func TestMuxInterceptors(t *testing.T) {
 			mux: func() *safehttp.ServeMux {
 				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
 
-				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+				registeredHandler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 				})
 				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
@@ -258,7 +258,7 @@ func TestMuxInterceptors(t *testing.T) {
 				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
 				mux.Install("test", internalErrorInterceptor{})
 
-				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+				registeredHandler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 				})
 				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
@@ -278,7 +278,7 @@ func TestMuxInterceptors(t *testing.T) {
 				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
 				mux.Install("claim", &claimHeaderInterceptor{headerToClaim: "Foo"})
 
-				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+				registeredHandler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 					p := w.Interceptor("claim").(*claimHeaderInterceptor)
 					p.SetHeader("bar")
 					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
@@ -297,7 +297,7 @@ func TestMuxInterceptors(t *testing.T) {
 				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
 				mux.Install("panic", panickingInterceptor{})
 
-				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+				registeredHandler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 				})
 				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
diff --git a/safehttp/response_writer_test.go b/safehttp/response_writer_test.go
index e4a1a7b..2148f69 100644
--- a/safehttp/response_writer_test.go
+++ b/safehttp/response_writer_test.go
@@ -17,9 +17,12 @@ package safehttp_test
 import (
 	"strings"
 	"testing"
+	"text/template"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+	"github.com/google/safehtml"
 )
 
 func TestResponseWriterSetCookie(t *testing.T) {
@@ -50,3 +53,59 @@ func TestResponseWriterSetInvalidCookie(t *testing.T) {
 		t.Error("rw.SetCookie(c) got: nil want: error")
 	}
 }
+
+func TestResponseWriterWriteTwicePanic(t *testing.T) {
+	tests := []struct {
+		name  string
+		write func(w *safehttp.ResponseWriter)
+	}{
+		{
+			name: "Call Write twice",
+			write: func(w *safehttp.ResponseWriter) {
+				w.Write(safehtml.HTMLEscaped("<h1>Escaped, so not really a heading</h1>"))
+				w.Write(safehtml.HTMLEscaped("<h1>Escaped, so not really a heading</h1>"))
+			},
+		},
+		{
+			name: "Call WriteTemplate twice",
+			write: func(w *safehttp.ResponseWriter) {
+				w.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+				w.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+			},
+		},
+		{
+			name: "Call ClientError twice",
+			write: func(w *safehttp.ResponseWriter) {
+				w.ClientError(safehttp.StatusBadRequest)
+				w.ClientError(safehttp.StatusBadRequest)
+			},
+		},
+		{
+			name: "Call ServerError twice",
+			write: func(w *safehttp.ResponseWriter) {
+				w.ServerError(safehttp.StatusInternalServerError)
+				w.ServerError(safehttp.StatusInternalServerError)
+			},
+		},
+		{
+			name: "Call Redirect twice",
+			write: func(w *safehttp.ResponseWriter) {
+				ir := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+				w.Redirect(ir, "/asdf", safehttp.StatusMovedPermanently)
+				w.Redirect(ir, "/asdf", safehttp.StatusMovedPermanently)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			w := safehttp.NewResponseWriter(testDispatcher{}, newResponseRecorder(&strings.Builder{}), nil)
+			defer func() {
+				if r := recover(); r == nil {
+					t.Errorf("tt.write(w) expected panic")
+				}
+			}()
+			tt.write(w)
+		})
+	}
+}
diff --git a/safehttp/safehttptest/recorder.go b/safehttp/safehttptest/recorder.go
index 0fc22b0..894dc99 100644
--- a/safehttp/safehttptest/recorder.go
+++ b/safehttp/safehttptest/recorder.go
@@ -49,7 +49,7 @@ func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Templat
 // mutations for later inspection in tests. The safehttp.ResponseWriter
 // should be passed as part of the handler function in tests.
 type ResponseRecorder struct {
-	safehttp.ResponseWriter
+	*safehttp.ResponseWriter
 	rw *responseWriter
 	b  *strings.Builder
 }
diff --git a/tests/integration/header/header_test.go b/tests/integration/header/header_test.go
index 969b5c0..c6254cc 100644
--- a/tests/integration/header/header_test.go
+++ b/tests/integration/header/header_test.go
@@ -67,7 +67,7 @@ func (r *responseRecorder) Write(data []byte) (int, error) {
 
 func TestAccessIncomingHeaders(t *testing.T) {
 	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+	mux.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)
 		}
@@ -90,7 +90,7 @@ func TestAccessIncomingHeaders(t *testing.T) {
 
 func TestChangingResponseHeaders(t *testing.T) {
 	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+	mux.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 b358f89..7907373 100644
--- a/tests/integration/hsts/hsts_test.go
+++ b/tests/integration/hsts/hsts_test.go
@@ -68,7 +68,7 @@ func (r *responseRecorder) Write(data []byte) (int, error) {
 func TestHSTSServeMuxInstall(t *testing.T) {
 	mux := safehttp.NewServeMux(&dispatcher{}, "foo.com")
 
-	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+	handler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 	})
 	mux.Handle("/asdf", safehttp.MethodGet, handler)
diff --git a/tests/integration/safehtml/safehtml_test.go b/tests/integration/safehtml/safehtml_test.go
index eb01a50..517295c 100644
--- a/tests/integration/safehtml/safehtml_test.go
+++ b/tests/integration/safehtml/safehtml_test.go
@@ -71,7 +71,7 @@ func (r *responseRecorder) Write(data []byte) (int, error) {
 
 func TestHandleRequestWrite(t *testing.T) {
 	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+	mux.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>"))
 	}))
 
@@ -91,7 +91,7 @@ func TestHandleRequestWrite(t *testing.T) {
 
 func TestHandleRequestWriteTemplate(t *testing.T) {
 	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
-	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		return rw.WriteTemplate(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 bb3a697..ea7fc45 100644
--- a/tests/integration/staticheaders/staticheaders_test.go
+++ b/tests/integration/staticheaders/staticheaders_test.go
@@ -74,7 +74,7 @@ func (r *responseRecorder) Write(data []byte) (int, error) {
 func TestServeMuxInstallStaticHeaders(t *testing.T) {
 	mux := safehttp.NewServeMux(dispatcher{}, "foo.com")
 
-	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+	handler := safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 	})
 	mux.Handle("/asdf", safehttp.MethodGet, handler)