Repository: google/go-safeweb (go)

Issue:
Add a Commit phase to the Interceptor interface
As some plugins might want to modify/add headers or modify the body (e.g. CSP/XSRF nonce injection in HTML templates) exactly before the response is written, the interceptor interface needs to support a Commit phase.

Pull request:
Added the Commit phase
Fixes #145

Added the `Commit` phase to the Interceptor interface. This will be called by the `safehttp.ResponseWriter` before a response is written to the underlying `http.ResponseWriter`, for all interceptors that were installed on the handler. If a response  is written in any of the `Commit` phases,  then the `Commit` phases of the remaining interceptor won't execute.

As the `Commit` phase takes a `safehttp.Response` as argument (e.g. for injecting nonces), we also created a `safehttp.TemplateResponse` interface that encapsulates a safe template and the data that is applied to it.

Co-authored-by: Mihali Mara <maramihali@google.com>


Patch:
diff --git a/safehttp/interceptor.go b/safehttp/interceptor.go
index 67abe56..f3cf1d0 100644
--- a/safehttp/interceptor.go
+++ b/safehttp/interceptor.go
@@ -23,4 +23,9 @@ type Interceptor interface {
 	// 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, cfg interface{}) Result
+
+	// Commit runs before the response is written by the Dispatcher. If an error
+	// is written to the ResponseWriter, then the Commit phases from the
+	// remaining interceptors won't execute.
+	Commit(w *ResponseWriter, r *IncomingRequest, resp Response, cfg interface{}) Result
 }
diff --git a/safehttp/mux.go b/safehttp/mux.go
index d91a855..9bcce06 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -188,10 +188,13 @@ type handlerWithInterceptors struct {
 }
 
 // ServeHTTP calls the Before method of all the interceptors and then calls the
-// underlying handler.
+// underlying handler. Any panics that occur during the Before phase, during handler
+// execution or during the Commit phase are recovered here and a 500 Internal Server
+// Error response is sent.
 func (h handlerWithInterceptors) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	rw := NewResponseWriter(h.disp, w)
 	ir := NewIncomingRequest(r)
+	rw := NewResponseWriter(h.disp, w, ir)
+	rw.handler = h
 
 	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.
 	// The reason is, we might need to run After/Commit stages of the interceptors before we
@@ -214,3 +217,22 @@ func (h handlerWithInterceptors) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		rw.NoContent()
 	}
 }
+
+// commitPhase calls the Commit phases of all the interceptors. This stage will
+// run before a response is written to the ResponseWriter. If a response is
+// written to the ResponseWriter in a Commit phase then the Commit phases of the
+// remaining interceptors won't execute.
+//
+// TODO: BIG WARNING, if ResponseWriter.Write and ResponseWriter.WriteTemplate
+// are called in Commit then this will recurse. CommitResponseWriter was an
+// attempt to prevent this by not giving access to Write and WriteTemplate in
+// the Commit phase.
+func (h handlerWithInterceptors) commitPhase(w *ResponseWriter, resp Response) {
+	for i := len(h.interceps) - 1; i >= 0; i-- {
+		ai := h.interceps[i]
+		ai.it.Commit(w, w.req, resp, ai.cfg)
+		if w.written {
+			return
+		}
+	}
+}
diff --git a/safehttp/plugins/csp/csp.go b/safehttp/plugins/csp/csp.go
index 1a39a3e..f718b69 100644
--- a/safehttp/plugins/csp/csp.go
+++ b/safehttp/plugins/csp/csp.go
@@ -42,11 +42,11 @@ func generateNonce() string {
 }
 
 // Policy defines a CSP policy.
-type Policy struct {
-	// serialize serializes this policy for use in a Content-Security-Policy header
+type Policy interface {
+	// Serialize serializes this policy for use in a Content-Security-Policy header
 	// or in a Content-Security-Policy-Report-Only header. A nonce will be provided
 	// to serialize which can be used in 'nonce-{random-nonce}' values in directives.
-	serialize func(nonce string) string
+	Serialize(nonce string) string
 }
 
 type ctxKey struct{}
@@ -61,10 +61,10 @@ func Nonce(ctx context.Context) (string, error) {
 	return v.(string), nil
 }
 
-// StrictCSPBuilder can be used to build a strict, nonce-based CSP.
+// StrictPolicy can be used to build a strict, nonce-based CSP.
 //
 // See https://csp.withgoogle.com/docs/strict-csp.html for more info.
-type StrictCSPBuilder struct {
+type StrictPolicy struct {
 	// NoStrictDynamic controls whether script-src should contain the 'strict-dynamic'
 	// value.
 	//
@@ -92,77 +92,77 @@ type StrictCSPBuilder struct {
 	Hashes []string
 }
 
-// Build creates a Policy based on the specified options.
-func (s StrictCSPBuilder) Build() Policy {
-	return Policy{
-		serialize: func(nonce string) string {
-			var b strings.Builder
-
-			// object-src 'none'; script-src 'unsafe-inline' 'nonce-{random}'
-			b.WriteString("object-src 'none'; script-src 'unsafe-inline' 'nonce-")
-			b.WriteString(nonce)
-			b.WriteByte('\'')
-
-			if !s.NoStrictDynamic {
-				b.WriteString(" 'strict-dynamic' https: http:")
-			}
-
-			if s.UnsafeEval {
-				b.WriteString(" 'unsafe-eval'")
-			}
-
-			for _, h := range s.Hashes {
-				b.WriteString(" '")
-				b.WriteString(h)
-				b.WriteByte('\'')
-			}
-
-			b.WriteString("; base-uri ")
-			if s.BaseURI == "" {
-				b.WriteString("'none'")
-			} else {
-				b.WriteString(s.BaseURI)
-			}
-
-			if s.ReportURI != "" {
-				b.WriteString("; report-uri ")
-				b.WriteString(s.ReportURI)
-			}
-
-			return b.String()
-		},
+// Serialize serializes this policy for use in a Content-Security-Policy header
+// or in a Content-Security-Policy-Report-Only header. A nonce will be provided
+// to Serialize which can be used in 'nonce-{random-nonce}' values in directives.
+func (s StrictPolicy) Serialize(nonce string) string {
+	var b strings.Builder
+
+	// object-src 'none'; script-src 'unsafe-inline' 'nonce-{random}'
+	b.WriteString("object-src 'none'; script-src 'unsafe-inline' 'nonce-")
+	b.WriteString(nonce)
+	b.WriteByte('\'')
+
+	if !s.NoStrictDynamic {
+		b.WriteString(" 'strict-dynamic' https: http:")
+	}
+
+	if s.UnsafeEval {
+		b.WriteString(" 'unsafe-eval'")
+	}
+
+	for _, h := range s.Hashes {
+		b.WriteString(" '")
+		b.WriteString(h)
+		b.WriteByte('\'')
+	}
+
+	b.WriteString("; base-uri ")
+	if s.BaseURI == "" {
+		b.WriteString("'none'")
+	} else {
+		b.WriteString(s.BaseURI)
+	}
+
+	if s.ReportURI != "" {
+		b.WriteString("; report-uri ")
+		b.WriteString(s.ReportURI)
 	}
+
+	return b.String()
+
 }
 
-// FramingPolicyBuilder can be used to create a new CSP policy with frame-ancestors
+// FramingPolicy can be used to create a new CSP policy with frame-ancestors
 // set to 'self'.
 //
 // TODO: allow relaxation on specific endpoints according to #77.
-type FramingPolicyBuilder struct {
+type FramingPolicy struct {
 	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
 	// directive will be set.
 	ReportURI string
 }
 
-// Build creates a Policy based on the specified options.
-func (f FramingPolicyBuilder) Build() Policy {
-	return Policy{
-		serialize: func(_ string) string {
-			var b strings.Builder
-			b.WriteString("frame-ancestors 'self'")
+// Serialize serializes this policy for use in a Content-Security-Policy header
+// or in a Content-Security-Policy-Report-Only header. A nonce will be provided
+// to Serialize which can be used in 'nonce-{random-nonce}' values in directives.
+func (f FramingPolicy) Serialize(nonce string) string {
+	var b strings.Builder
+	b.WriteString("frame-ancestors 'self'")
 
-			if f.ReportURI != "" {
-				b.WriteString("; report-uri ")
-				b.WriteString(f.ReportURI)
-			}
-
-			return b.String()
-		},
+	if f.ReportURI != "" {
+		b.WriteString("; report-uri ")
+		b.WriteString(f.ReportURI)
 	}
+
+	return b.String()
 }
 
 // Interceptor intercepts requests and applies CSP policies.
 type Interceptor struct {
+	// TODO: move it to a safehttp.Config implementation to enable per-handler
+	// configuration.
+
 	// Enforce specifies which policies will be set as the Content-Security-Policy
 	// header.
 	Enforce []Policy
@@ -176,8 +176,8 @@ type Interceptor struct {
 func Default(reportURI string) Interceptor {
 	return Interceptor{
 		Enforce: []Policy{
-			StrictCSPBuilder{ReportURI: reportURI}.Build(),
-			FramingPolicyBuilder{ReportURI: reportURI}.Build(),
+			StrictPolicy{ReportURI: reportURI},
+			FramingPolicy{ReportURI: reportURI},
 		},
 	}
 }
@@ -190,11 +190,11 @@ func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 
 	var CSPs []string
 	for _, p := range it.Enforce {
-		CSPs = append(CSPs, p.serialize(nonce))
+		CSPs = append(CSPs, p.Serialize(nonce))
 	}
 	var reportCSPs []string
 	for _, p := range it.ReportOnly {
-		reportCSPs = append(reportCSPs, p.serialize(nonce))
+		reportCSPs = append(reportCSPs, p.Serialize(nonce))
 	}
 
 	h := w.Header()
@@ -206,3 +206,7 @@ func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 
 	return safehttp.Result{}
 }
+
+func (it Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.Result{}
+}
diff --git a/safehttp/plugins/fetch_metadata/fetch_metadata.go b/safehttp/plugins/fetch_metadata/fetch_metadata.go
index 7a6b551..5b51efd 100644
--- a/safehttp/plugins/fetch_metadata/fetch_metadata.go
+++ b/safehttp/plugins/fetch_metadata/fetch_metadata.go
@@ -163,3 +163,7 @@ func (p *Plugin) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest)
 
 	return safehttp.Result{}
 }
+
+func (p *Plugin) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.Result{}
+}
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index aabe90d..6c64cc4 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -92,3 +92,7 @@ func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 	set([]string{value.String()})
 	return safehttp.NotWritten()
 }
+
+func (it Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.Result{}
+}
diff --git a/safehttp/plugins/staticheaders/staticheaders.go b/safehttp/plugins/staticheaders/staticheaders.go
index 2aa2f7e..5f748b6 100644
--- a/safehttp/plugins/staticheaders/staticheaders.go
+++ b/safehttp/plugins/staticheaders/staticheaders.go
@@ -33,3 +33,7 @@ func (Plugin) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cf
 	setXXP([]string{"0"})
 	return safehttp.NotWritten()
 }
+
+func (Plugin) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.Result{}
+}
diff --git a/safehttp/plugins/xsrf/xsrf.go b/safehttp/plugins/xsrf/xsrf.go
index 8a4afb4..ffc3c44 100644
--- a/safehttp/plugins/xsrf/xsrf.go
+++ b/safehttp/plugins/xsrf/xsrf.go
@@ -131,3 +131,7 @@ func (i *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 	r.SetContext(context.WithValue(r.Context(), tokenCtxKey{}, tok))
 	return safehttp.NotWritten()
 }
+
+func (i *Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.Result{}
+}
diff --git a/safehttp/response.go b/safehttp/response.go
index b181505..26f2870 100644
--- a/safehttp/response.go
+++ b/safehttp/response.go
@@ -23,3 +23,15 @@ type Response interface{}
 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.
+type TemplateResponse struct {
+	Template *Template
+	Data     *interface{}
+}
+
+// NoContentResponse is sent to the commit phase when it's initiated from
+// ResponseWriter.NoContent.
+type NoContentResponse struct{}
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index e8fc9ca..b5c9eba 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -26,17 +26,23 @@ type ResponseWriter struct {
 	// Having this field unexported is essential for security. Otherwise one can
 	// easily overwrite the struct bypassing all our safety guarantees.
 	header  Header
+	handler handlerWithInterceptors
+	req     *IncomingRequest
 	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) *ResponseWriter {
+// http.ResponseWriter and a reference to the current IncomingRequest being served.
+// The IncomingRequest will only be used by the commit phase, which only runs when
+// the ServeMux is used, and can be passed as nil in tests.
+// TODO: remove the IncomingRequest parameter.
+func NewResponseWriter(d Dispatcher, rw http.ResponseWriter, req *IncomingRequest) *ResponseWriter {
 	header := newHeader(rw.Header())
 	return &ResponseWriter{
 		d:      d,
 		rw:     rw,
 		header: header,
+		req:    req,
 	}
 }
 
@@ -55,6 +61,13 @@ func NotWritten() Result {
 
 // Write TODO
 func (w *ResponseWriter) Write(resp Response) Result {
+	if w.written {
+		panic("ResponseWriter was already written to")
+	}
+	w.handler.commitPhase(w, resp)
+	if w.written {
+		return Result{}
+	}
 	w.markWritten()
 	if err := w.d.Write(w.rw, resp); err != nil {
 		panic("error")
@@ -64,6 +77,13 @@ func (w *ResponseWriter) Write(resp Response) Result {
 
 // WriteTemplate TODO
 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})
+	if w.written {
+		return Result{}
+	}
 	w.markWritten()
 	if err := w.d.ExecuteTemplate(w.rw, t, data); err != nil {
 		panic("error")
@@ -73,6 +93,13 @@ func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 
 // NoContent responds with a 204 No Content response.
 func (w *ResponseWriter) NoContent() Result {
+	if w.written {
+		panic("ResponseWriter was already written to")
+	}
+	w.handler.commitPhase(w, NoContentResponse{})
+	if w.written {
+		return Result{}
+	}
 	w.markWritten()
 	w.rw.WriteHeader(int(StatusNoContent))
 	return Result{}
@@ -88,10 +115,10 @@ func (w *ResponseWriter) WriteError(code StatusCode) 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")
 	}
+	w.markWritten()
 	http.Redirect(w.rw, r.req, url, int(code))
 	return Result{}
 }


Test patch:
diff --git a/safehttp/mux_test.go b/safehttp/mux_test.go
index 2cf7159..0d208e8 100644
--- a/safehttp/mux_test.go
+++ b/safehttp/mux_test.go
@@ -174,12 +174,21 @@ func (p setHeaderInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.Inc
 	return safehttp.NotWritten()
 }
 
+func (p setHeaderInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.NotWritten()
+}
+
 type internalErrorInterceptor struct{}
 
 func (internalErrorInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
 	return w.WriteError(safehttp.StatusInternalServerError)
 }
 
+func (internalErrorInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	w.Header().Set("Foo", "this should not be reached")
+	return safehttp.NotWritten()
+}
+
 type claimHeaderInterceptor struct {
 	headerToClaim string
 }
@@ -192,6 +201,10 @@ func (p *claimHeaderInterceptor) Before(w *safehttp.ResponseWriter, r *safehttp.
 	return safehttp.NotWritten()
 }
 
+func (p *claimHeaderInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.NotWritten()
+}
+
 func claimInterceptorSetHeader(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, value string) {
 	f := r.Context().Value(claimCtxKey{}).(func([]string))
 	f([]string{value})
@@ -203,6 +216,32 @@ func (panickingInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.Incom
 	panic("bad")
 }
 
+func (panickingInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return safehttp.NotWritten()
+}
+
+type committerInterceptor struct{}
+
+func (committerInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
+	return safehttp.NotWritten()
+}
+
+func (committerInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	w.Header().Set("foo", "bar")
+	return safehttp.NotWritten()
+}
+
+type commitErroringInterceptor struct{}
+
+func (commitErroringInterceptor) Before(w *safehttp.ResponseWriter, _ *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
+	w.Header().Set("foo", "bar")
+	return safehttp.NotWritten()
+}
+
+func (commitErroringInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	return w.WriteError(safehttp.StatusInternalServerError)
+}
+
 func TestMuxInterceptors(t *testing.T) {
 	tests := []struct {
 		name        string
@@ -281,13 +320,30 @@ func TestMuxInterceptors(t *testing.T) {
 
 				return mux
 			}(),
-			wantStatus: 500,
+			wantStatus: safehttp.StatusInternalServerError,
 			wantHeaders: map[string][]string{
 				"Content-Type":           {"text/plain; charset=utf-8"},
 				"X-Content-Type-Options": {"nosniff"},
 			},
 			wantBody: "Internal Server Error\n",
 		},
+		{
+			name: "Commit phase sets header",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+				mux.Install(committerInterceptor{})
+
+				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)
+
+				return mux
+			}(),
+			wantStatus:  safehttp.StatusOK,
+			wantHeaders: map[string][]string{"Foo": {"bar"}},
+			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+		},
 	}
 
 	for _, tt := range tests {
@@ -337,6 +393,17 @@ func (p setHeaderConfigInterceptor) Before(w *safehttp.ResponseWriter, _ *safeht
 	return safehttp.Result{}
 }
 
+func (p setHeaderConfigInterceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	name := "Commit-Pizza"
+	value := "Hawaii"
+	if c, ok := cfg.(setHeaderConfig); ok {
+		name = "Commit-" + c.name
+		value = c.value
+	}
+	w.Header().Set(name, value)
+	return safehttp.NotWritten()
+}
+
 type noInterceptorConfig struct{}
 
 func (noInterceptorConfig) Match(i safehttp.Interceptor) bool {
@@ -355,14 +422,14 @@ func TestMuxInterceptorConfigs(t *testing.T) {
 			name:        "SetHeaderInterceptor with config",
 			config:      setHeaderConfig{name: "Foo", value: "Bar"},
 			wantStatus:  safehttp.StatusOK,
-			wantHeaders: map[string][]string{"Foo": {"Bar"}},
+			wantHeaders: map[string][]string{"Foo": {"Bar"}, "Commit-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"}},
+			wantHeaders: map[string][]string{"Pizza": {"Hawaii"}, "Commit-Pizza": {"Hawaii"}},
 			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
 		},
 	}
@@ -403,29 +470,51 @@ type interceptorOne struct{}
 
 func (interceptorOne) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
 	w.Header().Set("pizza", "diavola")
-	return safehttp.Result{}
+	return safehttp.NotWritten()
 }
 
-type interceptorTwo struct {
+func (interceptorOne) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	if w.Header().Get("Commit2") != "b" {
+		w.WriteError(safehttp.StatusInternalServerError)
+	}
+	w.Header().Set("Commit1", "a")
+	return safehttp.NotWritten()
 }
 
+type interceptorTwo struct{}
+
 func (interceptorTwo) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
 	if w.Header().Get("pizza") != "diavola" {
 		w.WriteError(safehttp.StatusInternalServerError)
 	}
 	w.Header().Set("spaghetti", "bolognese")
-	return safehttp.Result{}
+	return safehttp.NotWritten()
 }
 
-type interceptorThree struct {
+func (interceptorTwo) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	if w.Header().Get("Commit3") != "c" {
+		w.WriteError(safehttp.StatusInternalServerError)
+	}
+	w.Header().Set("Commit2", "b")
+	return safehttp.NotWritten()
 }
 
+type interceptorThree struct{}
+
 func (interceptorThree) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg interface{}) safehttp.Result {
 	if w.Header().Get("spaghetti") != "bolognese" {
 		w.WriteError(safehttp.StatusInternalServerError)
 	}
 	w.Header().Set("dessert", "tiramisu")
-	return safehttp.Result{}
+	return safehttp.NotWritten()
+}
+
+func (interceptorThree) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg interface{}) safehttp.Result {
+	if w.Header().Get("Dessert") != "tiramisu" {
+		w.WriteError(safehttp.StatusInternalServerError)
+	}
+	w.Header().Set("Commit3", "c")
+	return safehttp.NotWritten()
 }
 
 func TestMuxDeterministicInterceptorOrder(t *testing.T) {
@@ -453,6 +542,9 @@ func TestMuxDeterministicInterceptorOrder(t *testing.T) {
 		"Dessert":   {"tiramisu"},
 		"Pizza":     {"diavola"},
 		"Spaghetti": {"bolognese"},
+		"Commit1":   {"a"},
+		"Commit2":   {"b"},
+		"Commit3":   {"c"},
 	}
 	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/plugins/csp/csp_test.go b/safehttp/plugins/csp/csp_test.go
index 2d13568..f55a1e1 100644
--- a/safehttp/plugins/csp/csp_test.go
+++ b/safehttp/plugins/csp/csp_test.go
@@ -48,62 +48,62 @@ func TestSerialize(t *testing.T) {
 	}{
 		{
 			name:       "StrictCSP",
-			policy:     StrictCSPBuilder{}.Build(),
+			policy:     StrictPolicy{},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http:; base-uri 'none'",
 		},
 		{
 			name:       "StrictCSP with no strict-dynamic",
-			policy:     StrictCSPBuilder{NoStrictDynamic: true}.Build(),
+			policy:     StrictPolicy{NoStrictDynamic: true},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret'; base-uri 'none'",
 		},
 		{
 			name:       "StrictCSP with unsafe-eval",
-			policy:     StrictCSPBuilder{UnsafeEval: true}.Build(),
+			policy:     StrictPolicy{UnsafeEval: true},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http: 'unsafe-eval'; base-uri 'none'",
 		},
 		{
 			name:       "StrictCSP with set base-uri",
-			policy:     StrictCSPBuilder{BaseURI: "https://example.com"}.Build(),
+			policy:     StrictPolicy{BaseURI: "https://example.com"},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http:; base-uri https://example.com",
 		},
 		{
 			name:       "StrictCSP with report-uri",
-			policy:     StrictCSPBuilder{ReportURI: "https://example.com/collector"}.Build(),
+			policy:     StrictPolicy{ReportURI: "https://example.com/collector"},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http:; base-uri 'none'; report-uri https://example.com/collector",
 		},
 		{
 			name: "StrictCSP with one hash",
-			policy: StrictCSPBuilder{Hashes: []string{
+			policy: StrictPolicy{Hashes: []string{
 				"sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=",
-			}}.Build(),
+			}},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http: 'sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M='; base-uri 'none'",
 		},
 		{
 			name: "StrictCSP with multiple hashes",
-			policy: StrictCSPBuilder{Hashes: []string{
+			policy: StrictPolicy{Hashes: []string{
 				"sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=",
 				"sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=",
-			}}.Build(),
+			}},
 			wantString: "object-src 'none'; script-src 'unsafe-inline' 'nonce-super-secret' 'strict-dynamic' https: http: 'sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=' 'sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M='; base-uri 'none'",
 		},
 		{
 			name:       "FramingCSP",
-			policy:     FramingPolicyBuilder{}.Build(),
+			policy:     FramingPolicy{},
 			wantString: "frame-ancestors 'self'",
 		},
 		{
 			name:       "FramingCSP with report-uri",
-			policy:     FramingPolicyBuilder{ReportURI: "httsp://example.com/collector"}.Build(),
+			policy:     FramingPolicy{ReportURI: "httsp://example.com/collector"},
 			wantString: "frame-ancestors 'self'; report-uri httsp://example.com/collector",
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			s := tt.policy.serialize("super-secret")
+			s := tt.policy.Serialize("super-secret")
 
 			if s != tt.wantString {
-				t.Errorf("tt.policy.serialize() got: %q want: %q", s, tt.wantString)
+				t.Errorf("tt.policy.Serialize() got: %q want: %q", s, tt.wantString)
 			}
 		})
 	}
@@ -144,7 +144,7 @@ func TestBefore(t *testing.T) {
 			name: "StrictCSP Report Only",
 			interceptor: Interceptor{
 				ReportOnly: []Policy{
-					StrictCSPBuilder{ReportURI: "https://example.com/collector"}.Build(),
+					StrictPolicy{ReportURI: "https://example.com/collector"},
 				},
 			},
 			wantReportOnlyPolicy: []string{
@@ -156,7 +156,7 @@ func TestBefore(t *testing.T) {
 			name: "FramingCSP Report Only",
 			interceptor: Interceptor{
 				ReportOnly: []Policy{
-					FramingPolicyBuilder{ReportURI: "https://example.com/collector"}.Build(),
+					FramingPolicy{ReportURI: "https://example.com/collector"},
 				},
 			},
 			wantReportOnlyPolicy: []string{"frame-ancestors 'self'; report-uri https://example.com/collector"},
diff --git a/safehttp/response_writer_test.go b/safehttp/response_writer_test.go
index 9a0cc11..d8ee1cc 100644
--- a/safehttp/response_writer_test.go
+++ b/safehttp/response_writer_test.go
@@ -27,7 +27,7 @@ import (
 
 func TestResponseWriterSetCookie(t *testing.T) {
 	rr := newResponseRecorder(&strings.Builder{})
-	rw := safehttp.NewResponseWriter(testDispatcher{}, rr)
+	rw := safehttp.NewResponseWriter(testDispatcher{}, rr, nil)
 
 	c := safehttp.NewCookie("foo", "bar")
 	err := rw.SetCookie(c)
@@ -45,7 +45,7 @@ func TestResponseWriterSetCookie(t *testing.T) {
 
 func TestResponseWriterSetInvalidCookie(t *testing.T) {
 	rr := newResponseRecorder(&strings.Builder{})
-	rw := safehttp.NewResponseWriter(testDispatcher{}, rr)
+	rw := safehttp.NewResponseWriter(testDispatcher{}, rr, nil)
 
 	c := safehttp.NewCookie("f=oo", "bar")
 	err := rw.SetCookie(c)
@@ -99,7 +99,7 @@ func TestResponseWriterWriteTwicePanic(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			w := safehttp.NewResponseWriter(testDispatcher{}, newResponseRecorder(&strings.Builder{}))
+			w := safehttp.NewResponseWriter(testDispatcher{}, newResponseRecorder(&strings.Builder{}), nil)
 			defer func() {
 				if r := recover(); r == nil {
 					t.Errorf("tt.write(w) expected panic")
diff --git a/safehttp/safehttptest/recorder.go b/safehttp/safehttptest/recorder.go
index 2941b95..894dc99 100644
--- a/safehttp/safehttptest/recorder.go
+++ b/safehttp/safehttptest/recorder.go
@@ -61,7 +61,7 @@ func NewResponseRecorder() *ResponseRecorder {
 	return &ResponseRecorder{
 		rw:             rw,
 		b:              &b,
-		ResponseWriter: safehttp.NewResponseWriter(testDispatcher{}, rw),
+		ResponseWriter: safehttp.NewResponseWriter(testDispatcher{}, rw, nil),
 	}
 }
 
@@ -73,7 +73,7 @@ func NewResponseRecorderFromDispatcher(d safehttp.Dispatcher) *ResponseRecorder
 	return &ResponseRecorder{
 		rw:             rw,
 		b:              &b,
-		ResponseWriter: safehttp.NewResponseWriter(d, rw),
+		ResponseWriter: safehttp.NewResponseWriter(d, rw, nil),
 	}
 }