Repository: google/go-safeweb (go)

Issue:
Create a XSRF plugin that blocks all state-changing requests
This would allow users of the framework to lock-in a stateless setup, and make sure potential state changing requests are not processed without a security review.

We currently have two anti-xsrf strategies: angular ([link](https://github.com/google/go-safeweb/tree/master/safehttp/plugins/xsrf/xsrfangular)) and HTML ([link](https://github.com/google/go-safeweb/tree/master/safehttp/plugins/xsrf/xsrfhtml)).

The idea would be to create a third one, called "xsrfblockall" or something similar that behaves exactly like `xsrfhtml` but it doesn't create or inject XSRF tokens, it just rejects all state changing requests.

The short summary is:
* Create a third XSRF plugin and copy the HTML-based one.
* Change the type to not require a key
* Remove the code inside the [`Commit`](https://github.com/google/go-safeweb/blob/master/safehttp/plugins/xsrf/xsrfhtml/xsrf.go#L107) implementation
* Change the [`Before`](https://github.com/google/go-safeweb/blob/master/safehttp/plugins/xsrf/xsrfhtml/xsrf.go#L60) implementation to just reject if the request is state changing.

Pull request:
feat: added xsrfblockall plugin
Fixes #337 

- [x] Tests pass
- [x] Appropriate changes to README are included in PR


Patch:
diff --git a/cmd/bancheck/doc.go b/cmd/bancheck/doc.go
index e2a6fea..8423617 100644
--- a/cmd/bancheck/doc.go
+++ b/cmd/bancheck/doc.go
@@ -16,7 +16,7 @@
 // See https://pkg.go.dev/github.com/google/go-safeweb/safehttp#hdr-Restricting_Risky_APIs
 // for a high level overview.
 //
-// Overview
+// # Overview
 //
 // Bancheck is a program that allows you to define risky APIs and check for their usage.
 // It can be used as part of the CI/CD pipeline to avoid common pitfalls and prevent
@@ -25,13 +25,13 @@
 // are needed for static code analysis. The tool resolves fully qualified function
 // and import names and checks them against a config file that defines risky APIs.
 //
-// Usage
+// # Usage
 //
 // Apart from the standard https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer flags
 // the command requires a config flag where a list of config files should be provided.
 // You can find a sample usage below.
 //
-// Config
+// # Config
 //
 // Config lets you specify which APIs should be banned, explain why they are risky to use
 // and allow a list of packages for which the check should be skipped.
@@ -42,59 +42,63 @@
 // i.e. one warning will still be returned.
 //
 // Example config:
-//  {
-// 		"functions": [
-// 			{
-// 				"name": "fmt.Printf",
-// 				"msg": "Banned by team A"
-// 			}
-// 		],
-// 		"imports": [
-// 			{
-// 				"name": "fmt",
-// 				"msg": "Banned by team A",
-//				"exemptions": [
-//					{
-//						"justification": "#yolo",
-//						"allowedPkg": "main"
-//					}
-//				]
-// 			}
-// 		]
-//  }
-//
-// Example
+//
+//	 {
+//			"functions": [
+//				{
+//					"name": "fmt.Printf",
+//					"msg": "Banned by team A"
+//				}
+//			],
+//			"imports": [
+//				{
+//					"name": "fmt",
+//					"msg": "Banned by team A",
+//					"exemptions": [
+//						{
+//							"justification": "#yolo",
+//							"allowedPkg": "main"
+//						}
+//					]
+//				}
+//			]
+//	 }
+//
+// # Example
 //
 // The example below shows a simple use case where "fmt" package and "fmt.Printf" function were banned
 // by two separate teams.
 //
 // main.go
-//  package main
 //
-//  import "fmt"
+//	package main
+//
+//	import "fmt"
 //
-//  func main() {
-//  	fmt.Printf("Hello")
-//  }
+//	func main() {
+//		fmt.Printf("Hello")
+//	}
 //
 // config.json
-//  {
-//  	"functions": [
-// 			{
-// 				"name": "fmt.Printf",
-// 	   			"msg": "Banned by team A"
-// 	  		}
-// 	 	],
-//   	"imports": [
-// 	  		{
-// 	   			"name": "fmt",
-// 	   			"msg": "Banned by team B"
-// 	  		}
-// 	 	],
-//  }
+//
+//	 {
+//	 	"functions": [
+//				{
+//					"name": "fmt.Printf",
+//		   			"msg": "Banned by team A"
+//		  		}
+//		 	],
+//	  	"imports": [
+//		  		{
+//		   			"name": "fmt",
+//		   			"msg": "Banned by team B"
+//		  		}
+//		 	],
+//	 }
 //
 // CLI usage
-//  $ ./bancheck -configs config.json main.go
-//  /go-safeweb/cmd/bancheck/test/main.go:3:8: Banned API found "fmt". Additional info: Banned by team B
-//  /go-safeweb/cmd/bancheck/test/main.go:6:6: Banned API found "fmt.Printf". Additional info: Banned by team A
+//
+//	$ ./bancheck -configs config.json main.go
+//	/go-safeweb/cmd/bancheck/test/main.go:3:8: Banned API found "fmt". Additional info: Banned by team B
+//	/go-safeweb/cmd/bancheck/test/main.go:6:6: Banned API found "fmt.Printf". Additional info: Banned by team A
 package main
diff --git a/examples/echo/echo.go b/examples/echo/echo.go
index 23e4fbb..10b3a89 100644
--- a/examples/echo/echo.go
+++ b/examples/echo/echo.go
@@ -15,8 +15,8 @@
 // echo implements a simple echo server which listents on localhost:8080.
 //
 // Endpoints:
-//  - /echo?message=YourMessage
-//  - /uptime
+//   - /echo?message=YourMessage
+//   - /uptime
 package main
 
 import (
diff --git a/safehttp/cookie.go b/safehttp/cookie.go
index 722eb26..f3136ec 100644
--- a/safehttp/cookie.go
+++ b/safehttp/cookie.go
@@ -28,9 +28,10 @@ type Cookie struct {
 
 // NewCookie creates a new Cookie with safe default settings.
 // Those safe defaults are:
-//  - Secure: true (if the framework is not in dev mode)
-//  - HttpOnly: true
-//  - SameSite: Lax
+//   - Secure: true (if the framework is not in dev mode)
+//   - HttpOnly: true
+//   - SameSite: Lax
+//
 // For more info about all the options, see:
 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
 func NewCookie(name, value string) *Cookie {
@@ -80,9 +81,9 @@ func (c *Cookie) SameSite(s SameSite) {
 
 // SetMaxAge sets the MaxAge attribute.
 //
-//  MaxAge = 0 means no 'Max-Age' attribute specified.
-//  MaxAge < 0 means delete cookie now, equivalently 'Max-Age: 0'
-//  MaxAge > 0 means Max-Age attribute present and given in seconds
+// - MaxAge = 0 means no 'Max-Age' attribute specified.
+// - MaxAge < 0 means delete cookie now, equivalently 'Max-Age: 0'
+// - MaxAge > 0 means Max-Age attribute present and given in seconds
 func (c *Cookie) SetMaxAge(maxAge int) {
 	c.wrapped.MaxAge = maxAge
 }
diff --git a/safehttp/doc.go b/safehttp/doc.go
index d294fe4..1e6a2b0 100644
--- a/safehttp/doc.go
+++ b/safehttp/doc.go
@@ -16,7 +16,7 @@
 // applications. See https://github.com/google/go-safeweb#readme to learn about
 // the goals and features.
 //
-// Safe Responses
+// # Safe Responses
 //
 // HTTP responses need to be crafted carefully in order to prevent common web
 // vulnerabilities like Cross-site Scripting (XSS). To help with this, we use
@@ -36,7 +36,7 @@
 // safe, we offer a way of configuring this in the framework. Whether a response
 // is considered safe or not is determined by the Dispatcher.
 //
-// Dispatcher
+// # Dispatcher
 //
 // An implementation of a Dispatcher should be provided by security experts in
 // your project. The Dispatcher is called for every write method of the
@@ -50,7 +50,7 @@
 // Warning: the security of the web application depends on a sound Dispatcher
 // implementation. Make sure it is security-reviewed and keep it simple.
 //
-// Interceptors
+// # Interceptors
 //
 // Not all security features can be implemented using the Dispatcher alone. For
 // instance, some requests should be rejected before they reach the handler in
@@ -61,7 +61,7 @@
 // the handler, and after the handler has committed a response. These are,
 // respectively, Before and Commit.
 //
-// Life of a Request
+// # Life of a Request
 //
 // To tie it all together, we will explain how a single request goes through the
 // framework.
@@ -99,51 +99,50 @@
 // It is safe to use defer statements for cleanup tasks (e.g. closing a file
 // that was used in a safehtml/template.Template response).
 //
-//  Stack trace of the flow:
+// Stack trace of the flow:
 //
-//  Mux.ServeHTTP()
-//  --+ Mux routes the request and checks the method.
-//  --+ InterceptorFoo.Before()
-//  --+ InterceptorBar.Before()
-//  --+ InterceptorBaz.Before()
-//  --+ Handler()
-//  ----+ ResponseWriter.Write
-//  ------+ InterceptorBaz.Commit()  // notice the inverted order
-//  ------+ InterceptorBar.Commit()
-//  ------+ InterceptorFoo.Commit()
-//  ------+ Dispatcher.Write()
-//  ----+ The result of the Response.Write() call is returned.
+//	Mux.ServeHTTP()
+//	--+ Mux routes the request and checks the method.
+//	--+ InterceptorFoo.Before()
+//	--+ InterceptorBar.Before()
+//	--+ InterceptorBaz.Before()
+//	--+ Handler()
+//	----+ ResponseWriter.Write
+//	------+ InterceptorBaz.Commit()  // notice the inverted order
+//	------+ InterceptorBar.Commit()
+//	------+ InterceptorFoo.Commit()
+//	------+ Dispatcher.Write()
+//	----+ The result of the Response.Write() call is returned.
 //
-// Error Responses
+// # Error Responses
 //
 // Error responses are written using ResponseWriter.WriteError. They go through
 // the usual Commit and Dispatcher phases.
 //
-// Configuring the Mux
+// # Configuring the Mux
 //
-// TODO
+// # TODO
 //
-// Incremental Adoption
+// # Incremental Adoption
 //
 // In order to migrate your service using http.Handlers to the safehttp package,
 // we recommend you start doing that one endpoint at a time. Use
 // RegisteredHandler to do this.
 //
-//  safeMuxConfig := /* configured ServeMuxConfig, including interceptors */
-//  safeMuxConfig.Handle("/bar", safehttp.MethodGET, barGETSafeHandler)
-//  safeMuxConfig.Handle("/bar", safehttp.MethodPOST, barPOSTSafeHandler)
-//  safeMuxConfig.Handle("/xyz", safehttp.MethodPOST, xyzSafeHandler)
-//  safeMux := safeMuxConfig.Mux()
+//	safeMuxConfig := /* configured ServeMuxConfig, including interceptors */
+//	safeMuxConfig.Handle("/bar", safehttp.MethodGET, barGETSafeHandler)
+//	safeMuxConfig.Handle("/bar", safehttp.MethodPOST, barPOSTSafeHandler)
+//	safeMuxConfig.Handle("/xyz", safehttp.MethodPOST, xyzSafeHandler)
+//	safeMux := safeMuxConfig.Mux()
 //
-//  // old, not yet migrated
-//  http.Handle("/foo", fooHandler)
+//	// old, not yet migrated
+//	http.Handle("/foo", fooHandler)
 //
-//  // new, migrated
-//  http.Handle("/bar", safehttp.RegisteredHandler(safeMux, "/bar"))
-//  http.Handle("/xyz", safehttp.RegisteredHandler(safeMux, "/xyz"))
+//	// new, migrated
+//	http.Handle("/bar", safehttp.RegisteredHandler(safeMux, "/bar"))
+//	http.Handle("/xyz", safehttp.RegisteredHandler(safeMux, "/xyz"))
 //
-//
-// Restricting Risky APIs
+// # Restricting Risky APIs
 //
 // Some APIs are easy-to-misuse in a security sensitive context. We choose to
 // restrict these and require a security review for their usage in order to
diff --git a/safehttp/migration.go b/safehttp/migration.go
index add0ce0..f2b9c75 100644
--- a/safehttp/migration.go
+++ b/safehttp/migration.go
@@ -25,7 +25,7 @@ import (
 // This method is helpful for migrating services incrementally, endpoint by
 // endpoint. The handler runs all the installed interceptors and the dispatcher.
 //
-// Important
+// # Important
 //
 // This function does not attempt to do any kind of path matching. If the
 // handler was registered using the ServeMuxConfig for a pattern "/foo/", this
diff --git a/safehttp/mux.go b/safehttp/mux.go
index 2a13a18..bb6d9d9 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -79,21 +79,21 @@ type ServeMux struct {
 // ServeHTTP dispatches the request to the handler whose method matches the
 // incoming request and whose pattern most closely matches the request URL.
 //
-//  For each incoming request:
-//  - [Before Phase] Interceptor.Before methods are called for every installed
-//    interceptor, until an interceptor writes to a ResponseWriter (including
-//    errors) or panics,
-//  - the handler is called after a [Before Phase] if no writes or panics occured,
-//  - the handler triggers the [Commit Phase] by writing to the ResponseWriter,
-//  - [Commit Phase] Interceptor.Commit methods run for every interceptor whose
-//    Before method was called,
-//  - [Dispatcher Phase] after the [Commit Phase], the Dispatcher's appropriate
-//    write method is called; the Dispatcher is responsible for determining whether
-//    the response is indeed safe and writing it,
-//  - if the handler attempts to write more than once, it is treated as an
-//    unrecoverable error; the request processing ends abrubptly with a panic and
-//    nothing else happens (note: this will change as soon as [After Phase] is
-//    introduced)
+//	For each incoming request:
+//	- [Before Phase] Interceptor.Before methods are called for every installed
+//	  interceptor, until an interceptor writes to a ResponseWriter (including
+//	  errors) or panics,
+//	- the handler is called after a [Before Phase] if no writes or panics occured,
+//	- the handler triggers the [Commit Phase] by writing to the ResponseWriter,
+//	- [Commit Phase] Interceptor.Commit methods run for every interceptor whose
+//	  Before method was called,
+//	- [Dispatcher Phase] after the [Commit Phase], the Dispatcher's appropriate
+//	  write method is called; the Dispatcher is responsible for determining whether
+//	  the response is indeed safe and writing it,
+//	- if the handler attempts to write more than once, it is treated as an
+//	  unrecoverable error; the request processing ends abrubptly with a panic and
+//	  nothing else happens (note: this will change as soon as [After Phase] is
+//	  introduced)
 //
 // Interceptors should NOT rely on the order they're run.
 func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index 6b80a93..a415bf1 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -21,11 +21,11 @@
 // only applies if the framework is not run in dev mode.
 //
 // More info:
-//  - MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
-//  - Wikipedia: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
-//  - RFC 6797: https://tools.ietf.org/html/rfc6797
+//   - MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
+//   - Wikipedia: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
+//   - RFC 6797: https://tools.ietf.org/html/rfc6797
 //
-// Usage
+// # Usage
 //
 // To construct the plugin with safe default settings, use Default. Otherwise,
 // create the Interceptor yourself.
@@ -74,9 +74,9 @@ var _ safehttp.Interceptor = Interceptor{}
 
 // Default creates a new HSTS interceptor with safe defaults.
 // These safe defaults are:
-//  - max-age set to 2 years,
-//  - includeSubDomains is enabled,
-//  - preload is disabled.
+//   - max-age set to 2 years,
+//   - includeSubDomains is enabled,
+//   - preload is disabled.
 func Default() Interceptor {
 	return Interceptor{MaxAge: 63072000 * time.Second} // two years in seconds
 }
diff --git a/safehttp/plugins/staticheaders/staticheaders.go b/safehttp/plugins/staticheaders/staticheaders.go
index 548b6e7..f00187a 100644
--- a/safehttp/plugins/staticheaders/staticheaders.go
+++ b/safehttp/plugins/staticheaders/staticheaders.go
@@ -23,7 +23,7 @@
 // are available and can introduce cross-site leaks vulnerabilities
 // (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).
 //
-// Usage
+// # Usage
 //
 // Install an instance of Interceptor using safehttp.ServerMux.Install.
 package staticheaders
@@ -39,8 +39,8 @@ type Interceptor struct{}
 var _ safehttp.Interceptor = Interceptor{}
 
 // Before claims and sets the following headers:
-//  - X-Content-Type-Options: nosniff
-//  - X-XSS-Protection: 0
+//   - X-Content-Type-Options: nosniff
+//   - X-XSS-Protection: 0
 func (Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
 	h := w.Header()
 	setXCTO := h.Claim("X-Content-Type-Options")
diff --git a/safehttp/plugins/xsrf/xsrfblockall/xsrf.go b/safehttp/plugins/xsrf/xsrfblockall/xsrf.go
new file mode 100644
index 0000000..65f5bb4
--- /dev/null
+++ b/safehttp/plugins/xsrf/xsrfblockall/xsrf.go
@@ -0,0 +1,49 @@
+// Copyright 2022 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 xsrfblockall
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+	"log"
+)
+
+// Interceptor provides protection against Cross-Site Request Forgery attacks
+// by rejecting all state-changing request.
+type Interceptor struct{}
+
+var _ safehttp.Interceptor = &Interceptor{}
+
+// Before rejects every state-changing request (all except GET, HEAD, and OPTIONS).
+func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
+	if xsrf.StatePreserving(r) {
+		return safehttp.NotWritten()
+	}
+
+	if safehttp.IsLocalDev() {
+		log.Println("xsrfblockall plugin blocked a state changing request")
+	}
+
+	return w.WriteError(safehttp.StatusForbidden)
+}
+
+// Commit does nothing.
+func (it *Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) {
+}
+
+// Match returns false since there are no supported configurations.
+func (*Interceptor) Match(safehttp.InterceptorConfig) bool {
+	return false
+}
diff --git a/safesql/safesql.go b/safesql/safesql.go
index 483b34c..c740b50 100644
--- a/safesql/safesql.go
+++ b/safesql/safesql.go
@@ -19,12 +19,16 @@
 // If uncheckedconversions and legacyconversions are not used and the sql package is forbidden this package guarantees that only compile-time constants will be
 // interpreted as SQL, thus preventing attacker-controlled strings to be accidentally executed.
 //
-// Migration Examples
+// # Migration Examples
 //
 // Code like the following is trivial to migrate from sql to safesql:
-// 	db.Query("SELECT ...", args...)
+//
+//	db.Query("SELECT ...", args...)
+//
 // The only change required would be to promote the string literal to a trusted string:
-// 	db.Query(safesql.New("SELECT ..."), args...)
+//
+//	db.Query(safesql.New("SELECT ..."), args...)
+//
 // For more complicated cases it might be needed to use the helper functions like Join and Concat.
 // If the queries for the service are stored in a trusted runtime-only source that cannot be controlled by a user
 // the uncheckedconversions package can be used to assert that those strings are under the programmer control.
@@ -37,7 +41,7 @@
 // The only relevant difference is that functions accept TrustedSQLString instances instead of plain "strings" and that some
 // dangerous methods have been removed.
 //
-// Explainer
+// # Explainer
 //
 // This package wraps the sql package and all methods that would normally take a string take a TrustedSQLString instead.
 // The constructor for TrustedSQLString takes a stringConstant as an argument, which is an unexported type constituted by a named string.


Test patch:
diff --git a/safehttp/plugins/xsrf/xsrfblockall/xsrf_test.go b/safehttp/plugins/xsrf/xsrfblockall/xsrf_test.go
new file mode 100644
index 0000000..ce995fe
--- /dev/null
+++ b/safehttp/plugins/xsrf/xsrfblockall/xsrf_test.go
@@ -0,0 +1,124 @@
+// Copyright 2022 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 xsrfblockall
+
+import (
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+func TestStateChanging(t *testing.T) {
+	test := []struct {
+		name       string
+		host       string
+		method     string
+		wantStatus safehttp.StatusCode
+	}{
+		{
+			name:       "POST request",
+			host:       "foo.com",
+			method:     safehttp.MethodPost,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "PUT request",
+			host:       "foo.com",
+			method:     safehttp.MethodPut,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "DELETE request",
+			host:       "foo.com",
+			method:     safehttp.MethodDelete,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "PATCH request",
+			host:       "foo.com",
+			method:     safehttp.MethodPatch,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "TRACE request",
+			host:       "foo.com",
+			method:     safehttp.MethodTrace,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "CONNECT request",
+			host:       "foo.com",
+			method:     safehttp.MethodConnect,
+			wantStatus: safehttp.StatusForbidden,
+		},
+	}
+
+	for _, test := range test {
+		t.Run(test.name, func(t *testing.T) {
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+			req := safehttptest.NewRequest(test.method, "https://foo.com/", nil)
+
+			i := Interceptor{}
+			i.Before(fakeRW, req, nil)
+
+			if got := rr.Code; got != int(test.wantStatus) {
+				t.Errorf("rr.Code: got %v, want %v", got, test.wantStatus)
+			}
+		})
+	}
+}
+
+func TestNonStateChanging(t *testing.T) {
+	test := []struct {
+		name       string
+		host       string
+		method     string
+		wantStatus safehttp.StatusCode
+	}{
+		{
+			name:       "GET request",
+			host:       "foo.com",
+			method:     safehttp.MethodGet,
+			wantStatus: safehttp.StatusOK,
+		},
+		{
+			name:       "HEAD request",
+			host:       "foo.com",
+			method:     safehttp.MethodHead,
+			wantStatus: safehttp.StatusOK,
+		},
+		{
+			name:       "OPTIONS request",
+			host:       "foo.com",
+			method:     safehttp.MethodOptions,
+			wantStatus: safehttp.StatusOK,
+		},
+	}
+
+	for _, test := range test {
+		t.Run(test.name, func(t *testing.T) {
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+			req := safehttptest.NewRequest(test.method, "https://foo.com/", nil)
+
+			i := Interceptor{}
+			i.Before(fakeRW, req, nil)
+
+			if got := rr.Code; got != int(test.wantStatus) {
+				t.Errorf("rr.Code: got %v, want %v", got, test.wantStatus)
+			}
+		})
+	}
+}