Repository: google/go-safeweb
Pull request #21 (2020-07-15 08:38:53)

Safe header type
This PR implements the safe header type and fixes some left over comments from #18 

mattiasgrenfeldt: Rebased on top of master. Ready to merge

Patch:
diff --git a/safehttp/header.go b/safehttp/header.go
new file mode 100644
index 0000000..5327192
--- /dev/null
+++ b/safehttp/header.go
@@ -0,0 +1,133 @@
+// 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 (
+	"errors"
+	"net/http"
+	"net/textproto"
+)
+
+// Header represents the key-value pairs in an HTTP header.
+// The keys will be in canonical form, as returned by
+// textproto.CanonicalMIMEHeaderKey.
+type Header struct {
+	wrapped   http.Header
+	immutable map[string]bool
+}
+
+func newHeader(h http.Header) Header {
+	return Header{wrapped: h, immutable: map[string]bool{}}
+}
+
+// MarkImmutable marks the header with the given name as immutable.
+// The name is first canonicalized using textproto.CanonicalMIMEHeaderKey.
+// This header is now read-only. If the header previously was
+// undefined, then it will forever be undefined. Other methods in
+// the struct can't write to, change or delete the header with this
+// name. These methods will instead fail when applied on an immutable
+// header.
+func (h Header) MarkImmutable(name string) {
+	name = textproto.CanonicalMIMEHeaderKey(name)
+	h.immutable[name] = true
+}
+
+// Set sets the header with the given name to the given value.
+// The name is first canonicalized using textproto.CanonicalMIMEHeaderKey.
+// This method first removes all other values associated with this
+// header before setting the new value. Returns an error when
+// applied on immutable headers or on the Set-Cookie header.
+func (h Header) Set(name, value string) error {
+	name = textproto.CanonicalMIMEHeaderKey(name)
+	if err := h.writableHeader(name); err != nil {
+		return err
+	}
+	h.wrapped.Set(name, value)
+	return nil
+}
+
+// Add adds a new header with the given name and the given value to
+// the collection of headers. The name is first canonicalized using
+// textproto.CanonicalMIMEHeaderKey. Returns an error when applied
+// on immutable headers or on the Set-Cookie header.
+func (h Header) Add(name, value string) error {
+	name = textproto.CanonicalMIMEHeaderKey(name)
+	if err := h.writableHeader(name); err != nil {
+		return err
+	}
+	h.wrapped.Add(name, value)
+	return nil
+}
+
+// Del deletes all headers with the given name. The name is first
+// canonicalized using textproto.CanonicalMIMEHeaderKey. Returns an
+// error when applied on immutable headers or on the Set-Cookie
+// header.
+func (h Header) Del(name string) error {
+	name = textproto.CanonicalMIMEHeaderKey(name)
+	if err := h.writableHeader(name); err != nil {
+		return err
+	}
+	h.wrapped.Del(name)
+	return nil
+}
+
+// Get returns the value of the first header with the given name.
+// The name is first canonicalized using textproto.CanonicalMIMEHeaderKey.
+// If no header exists with the given name then "" is returned.
+func (h Header) Get(name string) string {
+	return h.wrapped.Get(name)
+}
+
+// Values returns all the values of all the headers with the given name.
+// The name is first canonicalized using textproto.CanonicalMIMEHeaderKey.
+// The values are returned in the same order as they were sent in the request.
+// The values are returned as a copy of the original slice of strings in
+// the internal header map. This is to prevent modification of the original
+// slice. If no header exists with the given name then an empty slice is
+// returned.
+func (h Header) Values(name string) []string {
+	v := h.wrapped.Values(name)
+	clone := make([]string, len(v))
+	copy(clone, v)
+	return clone
+}
+
+// SetCookie adds the cookie provided as a Set-Cookie header in the header
+// collection. If the cookie is nil or cookie.Name is invalid, no header is
+// added. This is the only method that can modify the Set-Cookie header.
+// If other methods try to modify the header they will return errors.
+// TODO: Replace http.Cookie with safehttp.Cookie.
+func (h Header) SetCookie(cookie *http.Cookie) {
+	if v := cookie.String(); v != "" {
+		h.wrapped.Add("Set-Cookie", v)
+	}
+}
+
+// TODO: Add Write, WriteSubset and Clone when needed.
+
+// writableHeader assumes that the given name already has been canonicalized
+// using textproto.CanonicalMIMEHeaderKey.
+func (h Header) writableHeader(name string) error {
+	// TODO(@mattiasgrenfeldt, @kele, @empijei): Think about how this should
+	// work during legacy conversions.
+	if name == "Set-Cookie" {
+		return errors.New("can't write to Set-Cookie header")
+	}
+	if h.immutable[name] {
+		return errors.New("immutable header")
+	}
+	return nil
+}
diff --git a/safehttp/incoming_request.go b/safehttp/incoming_request.go
index 25df196..7056540 100644
--- a/safehttp/incoming_request.go
+++ b/safehttp/incoming_request.go
@@ -18,5 +18,10 @@ import "net/http"
 
 // IncomingRequest TODO
 type IncomingRequest struct {
-	req *http.Request
+	req    *http.Request
+	Header Header
+}
+
+func newIncomingRequest(req *http.Request) IncomingRequest {
+	return IncomingRequest{req: req, Header: newHeader(req.Header)}
 }
diff --git a/safehttp/prototype.go b/safehttp/prototype.go
index 5141de1..9542a6e 100644
--- a/safehttp/prototype.go
+++ b/safehttp/prototype.go
@@ -31,7 +31,7 @@ func NewMachinery(h HandleFunc, d Dispatcher) *Machinery {
 
 // HandleRequest TODO
 func (m *Machinery) HandleRequest(w http.ResponseWriter, req *http.Request) {
-	rw := ResponseWriter{d: m.d, rw: w}
-	ir := &IncomingRequest{req: req}
-	m.h(rw, ir)
+	rw := newResponseWriter(m.d, w)
+	ir := newIncomingRequest(req)
+	m.h(rw, &ir)
 }
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index 84debab..92b2acf 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -22,6 +22,16 @@ import (
 type ResponseWriter struct {
 	d  Dispatcher
 	rw http.ResponseWriter
+
+	// Having this field unexported is essential for
+	// security. Otherwise one can easily overwrite
+	// the struct bypassing all our safety guarantees.
+	header Header
+}
+
+func newResponseWriter(d Dispatcher, rw http.ResponseWriter) ResponseWriter {
+	header := newHeader(rw.Header())
+	return ResponseWriter{d: d, rw: rw, header: header}
 }
 
 // Result TODO
@@ -29,8 +39,7 @@ type Result struct{}
 
 // Write TODO
 func (w *ResponseWriter) Write(resp Response) Result {
-	err := w.d.Write(w.rw, resp)
-	if err != nil {
+	if err := w.d.Write(w.rw, resp); err != nil {
 		panic("error")
 	}
 	return Result{}
@@ -38,8 +47,7 @@ func (w *ResponseWriter) Write(resp Response) Result {
 
 // WriteTemplate TODO
 func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
-	err := w.d.ExecuteTemplate(w.rw, t, data)
-	if err != nil {
+	if err := w.d.ExecuteTemplate(w.rw, t, data); err != nil {
 		panic("error")
 	}
 	return Result{}
@@ -50,6 +58,13 @@ func (w *ResponseWriter) ServerError(code StatusCode, resp Response) Result {
 	return Result{}
 }
 
+// Header returns the collection of headers that will be set
+// on the response. Headers must be set before writing a
+// response (e.g. Write, WriteTemplate).
+func (w ResponseWriter) Header() Header {
+	return w.header
+}
+
 // Dispatcher TODO
 type Dispatcher interface {
 	Write(rw http.ResponseWriter, resp Response) error


Test patch:
diff --git a/safehttp/header_test.go b/safehttp/header_test.go
new file mode 100644
index 0000000..7b25dc0
--- /dev/null
+++ b/safehttp/header_test.go
@@ -0,0 +1,356 @@
+// 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 (
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestSet(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Set("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if got, want := h.Get("Foo-Key"), "Bar-Value"; got != want {
+		t.Errorf(`h.Get("Foo-Key") got: %q want %q`, got, want)
+	}
+}
+
+// TestSetCanonicalization verifies that names of headers
+// are canonicalized before being interpreted as header
+// names.
+// Note that the casing of the header name is different
+// when accessing and modifying the same header.
+func TestSetCanonicalization(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("fOo-KeY", "Bar-Value"); err != nil {
+		t.Errorf(`h.Set("fOo-KeY", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if got, want := h.Get("FoO-kEy"), "Bar-Value"; got != want {
+		t.Errorf(`h.Get("FoO-kEy") got: %q want %q`, got, want)
+	}
+}
+
+func TestSetSetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	cookie := &http.Cookie{Name: "x", Value: "y"}
+	h.SetCookie(cookie)
+	if err := h.Set("Set-Cookie", "x=y"); err == nil {
+		t.Error(`h.Set("Set-Cookie", "x=y") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{"x=y"}, h.Values("Set-Cookie")); diff != "" {
+		t.Errorf("h.Values(\"Set-Cookie\") mismatch (-want +got):\n%s", diff)
+	}
+}
+func TestSetEmptySetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("Set-Cookie", "x=y"); err == nil {
+		t.Error(`h.Set("Set-Cookie", "x=y") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Set-Cookie")); diff != "" {
+		t.Errorf("h.Values(\"Set-Cookie\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestSetImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("Foo-Key", "Pizza-Value"); err != nil {
+		t.Errorf(`h.Set("Foo-Key", "Pizza-Value") got: %v want: nil`, err)
+	}
+	h.MarkImmutable("Foo-Key")
+	if err := h.Set("Foo-Key", "Bar-Value"); err == nil {
+		t.Error(`h.Set("Foo-Key", "Bar-Value") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{"Pizza-Value"}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestSetEmptyImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	h.MarkImmutable("Foo-Key")
+	if err := h.Set("Foo-Key", "Bar-Value"); err == nil {
+		t.Error(`h.Set("Foo-Key", "Bar-Value") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestAdd(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Add("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Add("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if err := h.Add("Foo-Key", "Pizza-Value"); err != nil {
+		t.Errorf(`h.Add("Foo-Key", "Pizza-Value") got err: %v want: nil`, err)
+	}
+	if diff := cmp.Diff([]string{"Bar-Value", "Pizza-Value"}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+// TestAddCanonicalization verifies that names of headers
+// are canonicalized before being interpreted as header
+// names.
+// Note that the casing of the header name is different
+// when accessing and modifying the same header.
+func TestAddCanonicalization(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Add("fOo-KeY", "Bar-Value"); err != nil {
+		t.Errorf(`h.Add("fOo-KeY", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if err := h.Add("FoO-kEy", "Pizza-Value"); err != nil {
+		t.Errorf(`h.Add("FoO-kEy", "Pizza-Value") got err: %v want: nil`, err)
+	}
+	if diff := cmp.Diff([]string{"Bar-Value", "Pizza-Value"}, h.Values("fOO-KEY")); diff != "" {
+		t.Errorf("h.Values(\"fOO-KEY\")) mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestAddSetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	cookie := &http.Cookie{Name: "x", Value: "y"}
+	h.SetCookie(cookie)
+	if err := h.Add("Set-Cookie", "x=y"); err == nil {
+		t.Error(`h.Add("Set-Cookie", "x=y") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{"x=y"}, h.Values("Set-Cookie")); diff != "" {
+		t.Errorf("h.Values(\"Set-Cookie\") mismatch (-want +got):\n%s", diff)
+	}
+}
+func TestAddEmptySetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Add("Set-Cookie", "x=y"); err == nil {
+		t.Error(`h.Add("Set-Cookie", "x=y") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Set-Cookie")); diff != "" {
+		t.Errorf("h.Values(\"Set-Cookie\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestAddImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Add("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Add("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	h.MarkImmutable("Foo-Key")
+	if err := h.Add("Foo-Key", "Pizza-Value"); err == nil {
+		t.Error(`h.Add("Foo-Key", "Pizza-Value") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{"Bar-Value"}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestAddEmptyImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	h.MarkImmutable("Foo-Key")
+	if err := h.Add("Foo-Key", "Pizza-Value"); err == nil {
+		t.Error(`h.Add("Foo-Key", "Pizza-Value") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestDel(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Set("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if err := h.Del("Foo-Key"); err != nil {
+		t.Errorf(`h.Del("Foo-Key") got err: %v want: nil`, err)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+// TestDelCanonicalization verifies that names of headers
+// are canonicalized before being interpreted as header
+// names.
+// Note that the casing of the header name is different
+// when accessing and modifying the same header.
+func TestDelCanonicalization(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("fOo-KeY", "Bar-Value"); err != nil {
+		t.Errorf(`h.Set("fOo-KeY", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if err := h.Del("FoO-kEy"); err != nil {
+		t.Errorf(`h.Del("FoO-kEy") got err: %v want: nil`, err)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("FOO-kEY")); diff != "" {
+		t.Errorf("h.Values(\"FOO-kEY\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestDelSetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	cookie := &http.Cookie{Name: "x", Value: "y"}
+	h.SetCookie(cookie)
+	if err := h.Del("Set-Cookie"); err == nil {
+		t.Error(`h.Del("Set-Cookie") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{"x=y"}, h.Values("Set-Cookie")); diff != "" {
+		t.Errorf("h.Values(\"Set-Cookie\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestDelEmptySetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Del("Set-Cookie"); err == nil {
+		t.Error(`h.Del("Set-Cookie") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Set-Cookie")); diff != "" {
+		t.Errorf("h.Values(\"Set-Cookie\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestDelImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Set("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	h.MarkImmutable("Foo-Key")
+	if err := h.Del("Foo-Key"); err == nil {
+		t.Error(`h.Del("Foo-Key") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{"Bar-Value"}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestDelEmptyImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	h.MarkImmutable("Foo-Key")
+	if err := h.Del("Foo-Key"); err == nil {
+		t.Error(`h.Del("Foo-Key") got: nil want: error`)
+	}
+	if diff := cmp.Diff([]string{}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestSetCookie(t *testing.T) {
+	h := newHeader(http.Header{})
+	c := &http.Cookie{Name: "x", Value: "y"}
+	h.SetCookie(c)
+	if got, want := h.Get("Set-Cookie"), "x=y"; got != want {
+		t.Errorf(`h.Get("Set-Cookie") got: %q want: %q`, got, want)
+	}
+}
+
+func TestSetCookieInvalidName(t *testing.T) {
+	h := newHeader(http.Header{})
+	c := &http.Cookie{Name: "x=", Value: "y"}
+	h.SetCookie(c)
+	if got, want := h.Get("Set-Cookie"), ""; got != want {
+		t.Errorf(`h.Get("Set-Cookie") got: %q want: %q`, got, want)
+	}
+}
+
+// TestValuesModifyImmutable verifies that modifying the
+// slice returned by Values() doesn't modify the underlying
+// slice. The test ensures that Values() returns a copy
+// of the underlying slice.
+func TestValuesModifyImmutable(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Set("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Set("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	h.MarkImmutable("Foo-Key")
+	v := h.Values("Foo-Key")
+	if diff := cmp.Diff([]string{"Bar-Value"}, v); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+	v[0] = "Evil-Value"
+	if got, want := h.Get("Foo-Key"), "Bar-Value"; got != want {
+		t.Errorf(`h.Get("Foo-Key") got: %v want: %v`, got, want)
+	}
+}
+
+// TestValuesOrdering ensures that the Values() function
+// return the headers values in the order that they were
+// set.
+func TestValuesOrdering(t *testing.T) {
+	var tests = []struct {
+		name   string
+		values []string
+	}{
+		{
+			name:   "Bar Pizza",
+			values: []string{"Bar-Value", "Pizza-Value"},
+		},
+		{
+			name:   "Pizza Bar",
+			values: []string{"Pizza-Value", "Bar-Value"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			h := newHeader(http.Header{})
+			if err := h.Add("Foo-Key", tt.values[0]); err != nil {
+				t.Errorf(`h.Add("Foo-Key", tt.values[0]) got err: %v want: nil`, err)
+			}
+			if err := h.Add("Foo-Key", tt.values[1]); err != nil {
+				t.Errorf(`h.Add("Foo-Key", tt.values[1]) got err: %v want: nil`, err)
+			}
+			if diff := cmp.Diff(tt.values, h.Values("Foo-Key")); diff != "" {
+				t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestManyEqualKeyValuePairs(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Add("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Add("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if err := h.Add("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Add("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if diff := cmp.Diff([]string{"Bar-Value", "Bar-Value"}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+// TestAddSet tests that calling Set() after calling Add() will
+// correctly remove the previously added header before setting
+// the new one.
+func TestAddSet(t *testing.T) {
+	h := newHeader(http.Header{})
+	if err := h.Add("Foo-Key", "Bar-Value"); err != nil {
+		t.Errorf(`h.Add("Foo-Key", "Bar-Value") got err: %v want: nil`, err)
+	}
+	if err := h.Set("Foo-Key", "Pizza-Value"); err != nil {
+		t.Errorf(`h.Set("Foo-Key", "Pizza-Value") got err: %v want: nil`, err)
+	}
+	if diff := cmp.Diff([]string{"Pizza-Value"}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestValuesEmptyHeader(t *testing.T) {
+	h := newHeader(http.Header{})
+	if diff := cmp.Diff([]string{}, h.Values("Foo-Key")); diff != "" {
+		t.Errorf("h.Values(\"Foo-Key\") mismatch (-want +got):\n%s", diff)
+	}
+}
diff --git a/tests/integration/header/header_test.go b/tests/integration/header/header_test.go
new file mode 100644
index 0000000..309c2b0
--- /dev/null
+++ b/tests/integration/header/header_test.go
@@ -0,0 +1,82 @@
+// 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 header
+
+import (
+	"bufio"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+type dispatcher struct{}
+
+func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
+	switch x := resp.(type) {
+	case safehtml.HTML:
+		_, err := rw.Write([]byte(x.String()))
+		return err
+	default:
+		panic("not a safe response type")
+	}
+}
+
+func (dispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
+	return nil
+}
+
+func TestAccessIncomingHeaders(t *testing.T) {
+	m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+		if got, want := r.Header.Get("A"), "B"; got != want {
+			t.Errorf(`r.Header.Get("A") got: %v want: %v`, got, want)
+		}
+		return rw.Write(safehtml.HTMLEscaped("hello"))
+	}, &dispatcher{})
+
+	request := "GET / HTTP/1.1\r\n" +
+		"Host: localhost:8080\r\n" +
+		"A: B\r\n\r\n"
+	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(request)))
+	if err != nil {
+		t.Fatalf("http.ReadRequest() got err: %v", err)
+	}
+	recorder := httptest.NewRecorder()
+
+	m.HandleRequest(recorder, req)
+}
+
+func TestChangingResponseHeaders(t *testing.T) {
+	m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+		rw.Header().Set("pIZZA", "Pasta")
+		return rw.Write(safehtml.HTMLEscaped("hello"))
+	}, &dispatcher{})
+
+	req := httptest.NewRequest("GET", "/", nil)
+	recorder := httptest.NewRecorder()
+
+	m.HandleRequest(recorder, req)
+
+	resp := recorder.Result()
+
+	want := []string{"Pasta"}
+	if diff := cmp.Diff(want, resp.Header["Pizza"]); diff != "" {
+		t.Errorf(`resp.Header["Pizza"] mismatch (-want +got):\n%s`, diff)
+	}
+}
diff --git a/tests/integration/safehtml/safehtml_test.go b/tests/integration/safehtml/safehtml_test.go
index 1e13a89..71e7e0a 100644
--- a/tests/integration/safehtml/safehtml_test.go
+++ b/tests/integration/safehtml/safehtml_test.go
@@ -27,7 +27,7 @@ import (
 
 type dispatcher struct{}
 
-func (d *dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
+func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
 	switch x := resp.(type) {
 	case safehtml.HTML:
 		_, err := rw.Write([]byte(x.String()))
@@ -37,7 +37,7 @@ func (d *dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error
 	}
 }
 
-func (d *dispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
+func (dispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
 	switch x := t.(type) {
 	case *template.Template:
 		return x.Execute(rw, data)