Repository: google/go-safeweb
Pull request #341 (2022-10-10 16:03:27)

Update plugins to reflect internal state.
This is a start to uniform the plugins structure to easily support visibility checks and restrictions.

This doesn't add any logic to the current internal plugins.

I will create a separate PR to update the other plugins we haven't forked internally to the same structure.

bsiegert: A suggestion, maybe for a future change: you could put all the unsafe plugins into an `unsafe` subdirectory.
empijei: > A suggestion, maybe for a future change: you could put all the unsafe plugins into an `unsafe` subdirectory.

We do not have an "unsafe plugin", those are just configurations, and we decided to put the configs for a plugin next to the plugin that uses them.
empijei: I also fixed the copyright plugin to support multiple years.

Thanks for pointing this out.

If you think this PR is okay please merge it :smile: 
bsiegert: It looks like you need to give me write access, or have @kele review it.

Patch:
diff --git a/.github/workflows/copyright_linter.sh b/.github/workflows/copyright_linter.sh
index 43dbf97..fd92f3e 100755
--- a/.github/workflows/copyright_linter.sh
+++ b/.github/workflows/copyright_linter.sh
@@ -4,7 +4,7 @@ lines=$(cat $1 | wc -l)
 extension=$2
 for f in $(find . -name "*.$extension")
 do
-    diff=$(head $f -n $lines | diff $1 -)
+    diff=$(head $f -n $lines | sed 's/20[0-9][0-9]/YEAR/g' | diff $1 -)
     if [ ! -z "$diff" ]
     then
         echo $f
diff --git a/.github/workflows/go-copyright-header.txt b/.github/workflows/go-copyright-header.txt
index b331916..864e447 100644
--- a/.github/workflows/go-copyright-header.txt
+++ b/.github/workflows/go-copyright-header.txt
@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// Copyright YEAR Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/.github/workflows/html-copyright-header.txt b/.github/workflows/html-copyright-header.txt
index 3a084a3..59a9bd2 100644
--- a/.github/workflows/html-copyright-header.txt
+++ b/.github/workflows/html-copyright-header.txt
@@ -1,5 +1,5 @@
 <!--
-Copyright 2020 Google LLC
+Copyright YEAR 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
diff --git a/.github/workflows/js-copyright-header.txt b/.github/workflows/js-copyright-header.txt
index d90783d..52d23df 100644
--- a/.github/workflows/js-copyright-header.txt
+++ b/.github/workflows/js-copyright-header.txt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Google LLC
+ * Copyright YEAR 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
@@ -11,4 +11,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
- 
\ No newline at end of file
+ 
diff --git a/examples/echo/security/web/web.go b/examples/echo/security/web/web.go
index 4987d7b..030b60c 100644
--- a/examples/echo/security/web/web.go
+++ b/examples/echo/security/web/web.go
@@ -26,6 +26,7 @@ import (
 	"github.com/google/go-safeweb/safehttp/plugins/cors"
 	"github.com/google/go-safeweb/safehttp/plugins/csp"
 	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/framing"
 	"github.com/google/go-safeweb/safehttp/plugins/hostcheck"
 	"github.com/google/go-safeweb/safehttp/plugins/hsts"
 	"github.com/google/go-safeweb/safehttp/plugins/staticheaders"
@@ -37,12 +38,12 @@ import (
 // installed for security reasons.
 // These include:
 //
-//  - Cross-Origin-Opener-Policy
-//  - Content-Security-Policy
-//  - Fetch Metadata
-//  - HSTS
-//  - CORS
-//  - Host checking (against DNS rebinding and request smuggling)
+//   - Cross-Origin-Opener-Policy
+//   - Content-Security-Policy
+//   - Fetch Metadata
+//   - HSTS
+//   - CORS
+//   - Host checking (against DNS rebinding and request smuggling)
 //
 // Warning: XSRF protection is currently missing due to
 // https://github.com/google/go-safeweb/issues/171.
@@ -50,11 +51,16 @@ func NewMuxConfig(addr string) *safehttp.ServeMuxConfig {
 	c := &safehttp.ServeMuxConfig{}
 
 	c.Intercept(coop.Default(""))
-	c.Intercept(csp.Default(""))
-	c.Intercept(&fetchmetadata.Interceptor{})
 	c.Intercept(staticheaders.Interceptor{})
-
+	for _, i := range csp.Default("") {
+		c.Intercept(i)
+	}
 	c.Intercept(hsts.Default())
+
+	for _, i := range framing.Interceptors("") {
+		c.Intercept(i)
+	}
+	c.Intercept(fetchmetadata.ResourceIsolationPolicy())
 	c.Intercept(cors.Default())
 	c.Intercept(hostcheck.New(addr))
 	return c
@@ -64,10 +70,10 @@ func NewMuxConfig(addr string) *safehttp.ServeMuxConfig {
 // installed for security reasons.
 // These include:
 //
-//  - Cross-Origin-Opener-Policy
-//  - Content-Security-Policy
-//  - Fetch Metadata
-//  - Host checking (against DNS rebinding and request smuggling)
+//   - Cross-Origin-Opener-Policy
+//   - Content-Security-Policy
+//   - Fetch Metadata
+//   - Host checking (against DNS rebinding and request smuggling)
 //
 // It DOES NOT include HSTS or CORS, as these are difficult to setup in a
 // development environment.
@@ -78,8 +84,10 @@ func NewMuxConfigDev(port int) *safehttp.ServeMuxConfig {
 	c := &safehttp.ServeMuxConfig{}
 
 	c.Intercept(coop.Default(""))
-	c.Intercept(csp.Default(""))
-	c.Intercept(&fetchmetadata.Interceptor{})
+	for _, i := range csp.Default("") {
+		c.Intercept(i)
+	}
+	c.Intercept(fetchmetadata.ResourceIsolationPolicy())
 	c.Intercept(staticheaders.Interceptor{})
 
 	addr := fmt.Sprintf("localhost:%d", port)
diff --git a/examples/sample-application/secure/mux.go b/examples/sample-application/secure/mux.go
index dae64bb..ac7ab32 100644
--- a/examples/sample-application/secure/mux.go
+++ b/examples/sample-application/secure/mux.go
@@ -22,6 +22,7 @@ import (
 	"github.com/google/go-safeweb/safehttp/plugins/coop"
 	"github.com/google/go-safeweb/safehttp/plugins/csp"
 	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/framing"
 	"github.com/google/go-safeweb/safehttp/plugins/hostcheck"
 	"github.com/google/go-safeweb/safehttp/plugins/hsts"
 	"github.com/google/go-safeweb/safehttp/plugins/staticheaders"
@@ -34,13 +35,20 @@ import (
 // NewMuxConfig creates a safe ServeMuxConfig.
 func NewMuxConfig(db *storage.DB, addr string) *safehttp.ServeMuxConfig {
 	c := safehttp.NewServeMuxConfig(dispatcher{})
+
 	c.Intercept(coop.Default(""))
-	c.Intercept(csp.Default(""))
-	c.Intercept(&fetchmetadata.Interceptor{})
-	c.Intercept(hostcheck.New(addr))
-	c.Intercept(hsts.Default())
 	c.Intercept(staticheaders.Interceptor{})
+	for _, i := range csp.Default("") {
+		c.Intercept(i)
+	}
+	c.Intercept(hsts.Default())
+
+	for _, i := range framing.Interceptors("") {
+		c.Intercept(i)
+	}
+	c.Intercept(fetchmetadata.ResourceIsolationPolicy())
 	c.Intercept(&xsrfhtml.Interceptor{SecretAppKey: "secret-key-that-should-not-be-in-sources"})
 	c.Intercept(auth.Interceptor{DB: db})
+	c.Intercept(hostcheck.New(addr))
 	return c
 }
diff --git a/examples/trustedtypes/server.go b/examples/trustedtypes/server.go
index 867e599..dc49745 100644
--- a/examples/trustedtypes/server.go
+++ b/examples/trustedtypes/server.go
@@ -15,8 +15,8 @@
 // Implements a simple server presenting DOM XSS protection with Trusted Types.
 //
 // Endpoints:
-//  - /safe#<script>alert(1)</script>
-//  - /unsafe#<script>alert(1)</script>
+//   - /safe#<script>alert(1)</script>
+//   - /unsafe#<script>alert(1)</script>
 package main
 
 import (
@@ -36,7 +36,9 @@ func main() {
 	port := "8080"
 	addr := net.JoinHostPort(host, port)
 	var mc safehttp.ServeMuxConfig
-	mc.Intercept(csp.Default(""))
+	for _, i := range csp.Default("") {
+		mc.Intercept(i)
+	}
 	mux := mc.Mux()
 
 	safeTmplSrc, _ := template.TrustedSourceFromConstantDir("", template.TrustedSource{}, "safe.html")
diff --git a/safehttp/defaults/defaults.go b/safehttp/defaults/defaults.go
index 146d982..2a63687 100644
--- a/safehttp/defaults/defaults.go
+++ b/safehttp/defaults/defaults.go
@@ -22,6 +22,7 @@ import (
 	"github.com/google/go-safeweb/safehttp/plugins/coop"
 	"github.com/google/go-safeweb/safehttp/plugins/csp"
 	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/framing"
 	"github.com/google/go-safeweb/safehttp/plugins/hostcheck"
 	"github.com/google/go-safeweb/safehttp/plugins/hsts"
 	"github.com/google/go-safeweb/safehttp/plugins/staticheaders"
@@ -42,15 +43,24 @@ func ServeMuxConfig(hosts []string, xsrfKey string) (*safehttp.ServeMuxConfig, e
 	}
 
 	c := safehttp.NewServeMuxConfig(nil)
-	// TODO(clap): add a report group once we support reporting.
+
+	// Non-blocking:
+	c.Intercept(staticheaders.Interceptor{})
+	c.Intercept(hsts.Default())
+	// TODO(empijei): add a report group once we support reporting.
 	c.Intercept(coop.Default(""))
-	// TODO(clap): add a report-uri once we support reporting.
-	// TODO(clap): find a way to make the FramingPolicy here work together with the Framing plugin once we have it.
-	c.Intercept(csp.Default(""))
-	c.Intercept(&fetchmetadata.Interceptor{})
+	// TODO(empijei): add a report-uri once we support reporting.
+	for _, i := range csp.Default("") {
+		c.Intercept(i)
+	}
+
+	// Blocking:
 	c.Intercept(hostcheck.New(hosts...))
-	c.Intercept(hsts.Default())
-	c.Intercept(staticheaders.Interceptor{})
+	c.Intercept(fetchmetadata.ResourceIsolationPolicy())
 	c.Intercept(&xsrfhtml.Interceptor{SecretAppKey: xsrfKey})
+	for _, i := range framing.Interceptors("") {
+		c.Intercept(i)
+	}
+
 	return c, nil
 }
diff --git a/safehttp/plugins/cors/cors.go b/safehttp/plugins/cors/cors.go
index edf4c03..d2c9563 100644
--- a/safehttp/plugins/cors/cors.go
+++ b/safehttp/plugins/cors/cors.go
@@ -16,6 +16,7 @@
 package cors
 
 import (
+	"log"
 	"net/textproto"
 	"strconv"
 	"strings"
@@ -35,10 +36,10 @@ var disallowedContentTypes = map[string]bool{
 //
 // For more info about CORS, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
 //
-// Constraints
+// # Constraints
 //
 // The content types "application/x-www-form-urlencoded", "multipart/form-data"
-// and "text/plain" are banned and will result in a  415 Unsupported Media Type
+// and "text/plain" are banned and will result in a 415 Unsupported Media Type
 // response.
 //
 // Each CORS request must contain the header "X-Cors: 1".
@@ -73,10 +74,10 @@ var _ safehttp.Interceptor = &Interceptor{}
 
 // Default creates a CORS Interceptor with default settings.
 // Those defaults are:
-//  - No Exposed Headers
-//  - No Allowed Headers
-//  - AllowCredentials: false
-//  - MaxAge: 5 seconds
+//   - No Exposed Headers
+//   - No Allowed Headers
+//   - AllowCredentials: false
+//   - MaxAge: 5 seconds
 func Default(allowedOrigins ...string) *Interceptor {
 	ao := map[string]bool{}
 	for _, o := range allowedOrigins {
@@ -104,21 +105,27 @@ func (it *Interceptor) SetAllowedHeaders(headers ...string) {
 // Before handles the IncomingRequest according to the settings specified in the
 // Interceptor and sets the appropriate subset of the following headers:
 //
-//  - Access-Control-Allow-Credentials
-//  - Access-Control-Allow-Headers
-//  - Access-Control-Allow-Methods
-//  - Access-Control-Allow-Origin
-//  - Access-Control-Expose-Headers
-//  - Access-Control-Max-Age
-//  - Vary
+//   - Access-Control-Allow-Credentials
+//   - Access-Control-Allow-Headers
+//   - Access-Control-Allow-Methods
+//   - Access-Control-Allow-Origin
+//   - Access-Control-Expose-Headers
+//   - Access-Control-Max-Age
+//   - Vary
 func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
 	origin := r.Header.Get("Origin")
 	if origin != "" && !it.AllowedOrigins[origin] {
+		if safehttp.IsLocalDev() {
+			log.Println("cors plugin blocked a request due to a mismatching Origin header.")
+		}
 		return w.WriteError(safehttp.StatusForbidden)
 	}
 	h := w.Header()
 	allowOrigin := h.Claim("Access-Control-Allow-Origin")
 	if h.IsClaimed("Vary") {
+		if safehttp.IsLocalDev() {
+			log.Println("cors plugin failed to claim the Vary header")
+		}
 		return w.WriteError(safehttp.StatusInternalServerError)
 	}
 
@@ -135,6 +142,9 @@ func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingReq
 	}
 
 	if status != 0 && status != safehttp.StatusNoContent {
+		if safehttp.IsLocalDev() {
+			log.Println("cors plugin blocked a potentially malicious request")
+		}
 		return w.WriteError(status)
 	}
 
diff --git a/safehttp/plugins/csp/csp.go b/safehttp/plugins/csp/csp.go
index 503c39f..41b672f 100644
--- a/safehttp/plugins/csp/csp.go
+++ b/safehttp/plugins/csp/csp.go
@@ -15,26 +15,31 @@
 // Package csp provides a safehttp.Interceptor which applies Content-Security Policies
 // to responses.
 //
-// Three default policies are provided:
-//  - A strict nonce based CSP
-//  - A framing policy which sets frame-ancestors to 'self'
-//  - A Trusted Types policy which makes usage of dangerous web API functions secure by default
+// These default policies are provided:
+//   - A strict nonce based CSP
+//   - A framing policy which sets frame-ancestors to 'self'
+//   - A Trusted Types policy which makes usage of dangerous web API functions secure by default
 package csp
 
+// TODO(empijei): add support for report-to and report groups.
+
 import (
 	"context"
-	"crypto/rand"
 	"encoding/base64"
 	"errors"
 	"fmt"
 	"strings"
 
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
 	"github.com/google/go-safeweb/safehttp/plugins/htmlinject"
 
 	"github.com/google/go-safeweb/safehttp"
 )
 
-var randReader = rand.Reader
+const (
+	responseHeaderKey           = "Content-Security-Policy"
+	responseHeaderReportOnlyKey = responseHeaderKey + "-Report-Only"
+)
 
 // nonceSize is the size of the nonces in bytes. According to the CSP3 spec it should
 // be larger than 16 bytes. 20 bytes was picked to be future proof.
@@ -43,141 +48,73 @@ const nonceSize = 20
 
 func generateNonce() string {
 	b := make([]byte, nonceSize)
-	_, err := randReader.Read(b)
+	_, err := internalunsafecsp.RandReader.Read(b)
 	if err != nil {
 		panic(fmt.Errorf("failed to generate entropy using crypto/rand/RandReader: %v", err))
 	}
 	return base64.StdEncoding.EncodeToString(b)
 }
 
-// Policy defines a CSP policy.
-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(nonce string) string
-}
+type key string
 
-type nonceKey struct{}
+const (
+	nonceKey   key = "csp-nonce"
+	headersKey key = "csp-headers"
+)
 
 // Nonce retrieves the nonce from the given context. If there is no nonce stored
 // in the context, an error will be returned.
 func Nonce(ctx context.Context) (string, error) {
-	v := safehttp.FlightValues(ctx).Get(nonceKey{})
+	v := safehttp.FlightValues(ctx).Get(nonceKey)
 	if v == nil {
 		return "", errors.New("no nonce in context")
 	}
 	return v.(string), nil
 }
 
-// StrictPolicy can be used to build a strict, nonce-based CSP.
-//
-// See https://csp.withgoogle.com/docs/strict-csp.html for more info.
-type StrictPolicy struct {
-	// NoStrictDynamic controls whether script-src should contain the 'strict-dynamic'
-	// value.
-	//
-	// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic
-	// for more info.
-	NoStrictDynamic bool
-	// UnsafeEval controls whether script-src should contain the 'unsafe-eval' value.
-	// If enabled, the eval() JavaScript function is allowed.
-	UnsafeEval bool
-	// BaseURI controls the base-uri directive. If BaseURI is an empty string the
-	// directive will be set to 'none'. The base-uri directive restricts the URLs
-	// which can be used in a document's <base> element.
-	//
-	// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri
-	// for more info.
-	BaseURI string
-	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
-	// directive will be set.
-	ReportURI string
-	// Hashes adds a set of hashes to script-src. An example of a hash would be:
-	//  sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=
-	// which is the SHA256 hash for the script "console.log(1)".
-	//
-	// For more info, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
-	Hashes []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'")
+// nonce retrieves the nonces from the request.
+// If none is available, one will be generated and added to it.
+func nonce(r *safehttp.IncomingRequest) string {
+	v := safehttp.FlightValues(r.Context()).Get(nonceKey)
+	var nonce string
+	if v == nil {
+		nonce = generateNonce()
+		safehttp.FlightValues(r.Context()).Put(nonceKey, nonce)
+	} else {
+		nonce = v.(string)
 	}
+	return nonce
+}
 
-	for _, h := range s.Hashes {
-		b.WriteString(" '")
-		b.WriteString(h)
-		b.WriteByte('\'')
+func claimedHeaders(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) (cspe func([]string), cspro func([]string)) {
+	type claimed struct {
+		cspe, cspro func([]string)
 	}
-
-	b.WriteString("; base-uri ")
-	if s.BaseURI == "" {
-		b.WriteString("'none'")
+	v := safehttp.FlightValues(r.Context()).Get(headersKey)
+	var c claimed
+	if v == nil {
+		h := w.Header()
+		cspe := h.Claim(responseHeaderKey)
+		cspro := h.Claim(responseHeaderReportOnlyKey)
+		c = claimed{cspe: cspe, cspro: cspro}
+		safehttp.FlightValues(r.Context()).Put(headersKey, c)
 	} else {
-		b.WriteString(s.BaseURI)
-	}
-
-	if s.ReportURI != "" {
-		b.WriteString("; report-uri ")
-		b.WriteString(s.ReportURI)
+		c = v.(claimed)
 	}
-
-	return b.String()
-
-}
-
-// 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 FramingPolicy struct {
-	// Specifies valid parents that may embed a page using: <frame>, <iframe>, <object>, <embed>, or <applet>.
-	// More details: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors#sources
-	Sources []string
-	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
-	// directive will be set.
-	ReportURI 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 (f FramingPolicy) Serialize(nonce string) string {
-	var b strings.Builder
-
-	b.WriteString(frameAncestors(f.Sources))
-	b.WriteString(report(f.ReportURI))
-
-	return strings.TrimSpace(b.String())
+	return c.cspe, c.cspro
 }
 
-func frameAncestors(sources []string) string {
-	var b strings.Builder
-	b.WriteString("frame-ancestors 'self'")
-
-	for _, s := range sources {
-		b.WriteString(" ")
-		b.WriteString(s)
-	}
-	b.WriteString("; ")
-
-	return b.String()
+// Policy defines a CSP policy.
+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.
+	// If a config has matched the interceptor, it will also be passed.
+	Serialize(nonce string, cfg safehttp.InterceptorConfig) string
+	// Match allows to match configurations that are specific to this policy.
+	Match(cfg safehttp.InterceptorConfig) bool
+	// Overridden is used to check if a configuration is overriding the policy.
+	Overridden(cfg safehttp.InterceptorConfig) (disabled, reportOnly bool)
 }
 
 func report(reportURI string) string {
@@ -192,80 +129,56 @@ func report(reportURI string) string {
 	return b.String()
 }
 
-// TrustedTypesPolicy policy can be used to create a new CSP which makes
-// dangerous web API functions secure by default.
-//
-// See https://web.dev/trusted-types for more info.
-type TrustedTypesPolicy struct {
-	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
-	// directive will be set.
-	ReportURI 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 (t TrustedTypesPolicy) Serialize(nonce string) string {
-	var b strings.Builder
-	b.WriteString("require-trusted-types-for 'script'")
-
-	if t.ReportURI != "" {
-		b.WriteString("; report-uri ")
-		b.WriteString(t.ReportURI)
-	}
-
-	return b.String()
-}
-
 // Interceptor intercepts requests and applies CSP policies.
+// Multiple interceptors can be installed at the same time.
 type Interceptor struct {
-	// TODO: move it to a safehttp.InterceptorConfig implementation to enable
-	// per-handler configuration.
-
-	// Enforce specifies which policies will be set as the Content-Security-Policy
-	// header.
-	Enforce []Policy
-	// ReportOnly specifies which policies will be set as the Content-Security-Policy-Report-Only
-	// header.
-	ReportOnly []Policy
+	// Policy is the policy the interceptor should enforce.
+	Policy Policy
+	// ReportOnly makes Policy be set report-only.
+	ReportOnly bool
 }
 
 var _ safehttp.Interceptor = Interceptor{}
 
-// Default creates a new CSP interceptor with a strict nonce-based policy, a framing policy
-// and a TrustedTypes policy. All in enforcement mode.
-func Default(reportURI string) Interceptor {
-	return Interceptor{
-		Enforce: []Policy{
-			StrictPolicy{ReportURI: reportURI},
-			FramingPolicy{ReportURI: reportURI},
-			TrustedTypesPolicy{ReportURI: reportURI},
-		},
+// Default creates new CSP interceptors with a strict nonce-based policy and a TrustedTypes policy,
+// all in enforcement mode.
+// Framing policies are installed by the framing interceptor.
+func Default(reportURI string) []Interceptor {
+	return []Interceptor{
+		{Policy: StrictPolicy{ReportURI: reportURI}},
+		{Policy: TrustedTypesPolicy{ReportURI: reportURI}},
 	}
 }
 
-// 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.InterceptorConfig) safehttp.Result {
-	nonce := generateNonce()
-	safehttp.FlightValues(r.Context()).Put(nonceKey{}, nonce)
-
-	var CSPs []string
-	for _, p := range it.Enforce {
-		CSPs = append(CSPs, p.Serialize(nonce))
+func (it Interceptor) processOverride(cfg safehttp.InterceptorConfig, nonce string) (enf, ro string) {
+	disabled, reportOnly := false, false
+	if it.Policy.Match(cfg) {
+		disabled, reportOnly = it.Policy.Overridden(cfg)
 	}
-	var reportCSPs []string
-	for _, p := range it.ReportOnly {
-		reportCSPs = append(reportCSPs, p.Serialize(nonce))
+	if disabled {
+		return "", ""
 	}
+	p := it.Policy.Serialize(nonce, cfg)
+	if reportOnly || it.ReportOnly {
+		return "", p
+	}
+	return p, ""
+}
 
-	h := w.Header()
-	setCSP := h.Claim("Content-Security-Policy")
-	setCSPReportOnly := h.Claim("Content-Security-Policy-Report-Only")
-
-	setCSP(CSPs)
-	setCSPReportOnly(reportCSPs)
-
+// 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, cfg safehttp.InterceptorConfig) safehttp.Result {
+	nonce := nonce(r)
+	enf, ro := it.processOverride(cfg, nonce)
+	setCSP, setCSPReportOnly := claimedHeaders(w, r)
+	if enf != "" {
+		prev := w.Header().Values(responseHeaderKey)
+		setCSP(append(prev, enf))
+	}
+	if ro != "" {
+		prev := w.Header().Values(responseHeaderReportOnlyKey)
+		setCSPReportOnly(append(prev, ro))
+	}
 	return safehttp.NotWritten()
 }
 
@@ -281,7 +194,7 @@ func (it Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.Incom
 	nonce, err := Nonce(r.Context())
 	if err != nil {
 		// The nonce should have been added in the Before stage and, if that is
-		// not the case, a server misconfiguration occured.
+		// not the case, a server misconfiguration occurred.
 		panic("no CSP nonce")
 	}
 
@@ -292,6 +205,6 @@ func (it Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.Incom
 }
 
 // Match returns false since there are no supported configurations.
-func (Interceptor) Match(safehttp.InterceptorConfig) bool {
-	return false
+func (it Interceptor) Match(cfg safehttp.InterceptorConfig) bool {
+	return it.Policy.Match(cfg)
 }
diff --git a/safehttp/plugins/csp/framing.go b/safehttp/plugins/csp/framing.go
new file mode 100644
index 0000000..4415a31
--- /dev/null
+++ b/safehttp/plugins/csp/framing.go
@@ -0,0 +1,82 @@
+// 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 csp
+
+import (
+	"strings"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+)
+
+// FramingPolicy can be used to create a new CSP policy with frame-ancestors
+// set to 'self'.
+//
+// To specify a list of allowed framing hostnames use interceptor configurations.
+type FramingPolicy struct {
+	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
+	// directive will be set.
+	ReportURI 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 (f FramingPolicy) Serialize(nonce string, cfg safehttp.InterceptorConfig) string {
+	var b strings.Builder
+
+	var allow []string
+	if a, ok := cfg.(internalunsafeframing.AllowList); ok {
+		allow = a.Hostnames
+	}
+	b.WriteString(frameAncestors(allow))
+	b.WriteString(report(f.ReportURI))
+
+	return strings.TrimSpace(b.String())
+}
+
+// Match matches strict policies overrides.
+func (FramingPolicy) Match(cfg safehttp.InterceptorConfig) bool {
+	switch cfg.(type) {
+	case internalunsafeframing.Disable, internalunsafeframing.AllowList:
+		return true
+	}
+	return false
+}
+
+// Overridden checks the override level.
+func (FramingPolicy) Overridden(cfg safehttp.InterceptorConfig) (disabled, reportOnly bool) {
+	switch c := cfg.(type) {
+	case internalunsafeframing.Disable:
+		return c.SkipReports, true
+	case internalunsafeframing.AllowList:
+		return false, c.ReportOnly
+	}
+	// This should not happen.
+	return false, false
+}
+
+func frameAncestors(sources []string) string {
+	var b strings.Builder
+	b.WriteString("frame-ancestors 'self'")
+
+	for _, s := range sources {
+		b.WriteString(" ")
+		b.WriteString(s)
+	}
+	b.WriteString("; ")
+
+	return b.String()
+}
diff --git a/safehttp/plugins/csp/internalunsafecsp/internalunsafecsp.go b/safehttp/plugins/csp/internalunsafecsp/internalunsafecsp.go
new file mode 100644
index 0000000..6940d79
--- /dev/null
+++ b/safehttp/plugins/csp/internalunsafecsp/internalunsafecsp.go
@@ -0,0 +1,35 @@
+// 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 internalunsafecsp is used internally to override CSP.
+package internalunsafecsp
+
+import "crypto/rand"
+
+// RandReader is a source of random.
+var RandReader = rand.Reader
+
+// DisableTrustedTypes switches TT to report-only.
+type DisableTrustedTypes struct {
+	// SkipReports completely disables TT.
+	SkipReports bool
+}
+
+// DisableStrict switches Strict CSP to report-only.
+type DisableStrict struct {
+	// SkipReports completely disables Strict CSP.
+	SkipReports bool
+}
+
+// Framing is overridden by the framing override.
diff --git a/safehttp/plugins/csp/internalunsafecsp/unsafestrictcsp/unsafestrictcsp.go b/safehttp/plugins/csp/internalunsafecsp/unsafestrictcsp/unsafestrictcsp.go
new file mode 100644
index 0000000..d317690
--- /dev/null
+++ b/safehttp/plugins/csp/internalunsafecsp/unsafestrictcsp/unsafestrictcsp.go
@@ -0,0 +1,29 @@
+// 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 unsafestrictcsp can be used to disable Strict CSP protections on specific handler registration.
+//
+// Usage of this package should require a security review.
+package unsafestrictcsp
+
+import "github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
+
+// Disable switches Strict CSP to report-only.
+// If skipReports is true, it will completely disable it.
+func Disable(reason string, skipReports bool) internalunsafecsp.DisableStrict {
+	if reason == "" {
+		panic("reason cannot be empty")
+	}
+	return internalunsafecsp.DisableStrict{SkipReports: skipReports}
+}
diff --git a/safehttp/plugins/csp/internalunsafecsp/unsafetrustedtypes/unsafetrustedtypes.go b/safehttp/plugins/csp/internalunsafecsp/unsafetrustedtypes/unsafetrustedtypes.go
new file mode 100644
index 0000000..19de986
--- /dev/null
+++ b/safehttp/plugins/csp/internalunsafecsp/unsafetrustedtypes/unsafetrustedtypes.go
@@ -0,0 +1,29 @@
+// 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  unsafetrustedtypes can be used to disable Trusted Types protections on specific handler registration.
+//
+// Usage of this package should require a security review.
+package unsafetrustedtypes
+
+import "github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
+
+// Disable switches the trusted types policy to report-only.
+// If skipReports is true, it will completely disable it.
+func Disable(reason string, skipReports bool) internalunsafecsp.DisableTrustedTypes {
+	if reason == "" {
+		panic("reason cannot be empty")
+	}
+	return internalunsafecsp.DisableTrustedTypes{SkipReports: skipReports}
+}
diff --git a/safehttp/plugins/csp/strict.go b/safehttp/plugins/csp/strict.go
new file mode 100644
index 0000000..55523fb
--- /dev/null
+++ b/safehttp/plugins/csp/strict.go
@@ -0,0 +1,105 @@
+// 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 csp
+
+import (
+	"strings"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
+)
+
+// StrictPolicy can be used to build a strict, nonce-based CSP.
+//
+// See https://csp.withgoogle.com/docs/strict-csp.html for more info.
+type StrictPolicy struct {
+	// NoStrictDynamic controls whether script-src should contain the 'strict-dynamic'
+	// value.
+	//
+	// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic
+	// for more info.
+	NoStrictDynamic bool
+	// UnsafeEval controls whether script-src should contain the 'unsafe-eval' value.
+	// If enabled, the eval() JavaScript function is allowed.
+	UnsafeEval bool
+	// BaseURI controls the base-uri directive. If BaseURI is an empty string the
+	// directive will be set to 'none'. The base-uri directive restricts the URLs
+	// which can be used in a document's <base> element.
+	//
+	// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri
+	// for more info.
+	BaseURI string
+	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
+	// directive will be set.
+	ReportURI string
+	// Hashes adds a set of hashes to script-src. An example of a hash would be:
+	//  sha256-CihokcEcBW4atb/CW/XWsvWwbTjqwQlE9nj9ii5ww5M=
+	// which is the SHA256 hash for the script "console.log(1)".
+	//
+	// For more info, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
+	Hashes []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, _ safehttp.InterceptorConfig) 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()
+}
+
+// Match matches strict policies overrides.
+func (StrictPolicy) Match(cfg safehttp.InterceptorConfig) bool {
+	_, ok := cfg.(internalunsafecsp.DisableStrict)
+	return ok
+}
+
+// Overridden checks the override level.
+func (StrictPolicy) Overridden(cfg safehttp.InterceptorConfig) (disabled, reportOnly bool) {
+	disable := cfg.(internalunsafecsp.DisableStrict)
+	return disable.SkipReports, true
+}
diff --git a/safehttp/plugins/csp/trustedtypes.go b/safehttp/plugins/csp/trustedtypes.go
new file mode 100644
index 0000000..8e2140a
--- /dev/null
+++ b/safehttp/plugins/csp/trustedtypes.go
@@ -0,0 +1,59 @@
+// 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 csp
+
+import (
+	"strings"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
+)
+
+// TrustedTypesPolicy policy can be used to create a new CSP which makes
+// dangerous web API functions secure by default.
+//
+// See https://web.dev/trusted-types for more info.
+type TrustedTypesPolicy struct {
+	// ReportURI controls the report-uri directive. If ReportUri is empty, no report-uri
+	// directive will be set.
+	ReportURI 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 (t TrustedTypesPolicy) Serialize(nonce string, _ safehttp.InterceptorConfig) string {
+	var b strings.Builder
+	b.WriteString("require-trusted-types-for 'script'")
+
+	if t.ReportURI != "" {
+		b.WriteString("; report-uri ")
+		b.WriteString(t.ReportURI)
+	}
+
+	return b.String()
+}
+
+// Match matches strict policies overrides.
+func (TrustedTypesPolicy) Match(cfg safehttp.InterceptorConfig) bool {
+	_, ok := cfg.(internalunsafecsp.DisableTrustedTypes)
+	return ok
+}
+
+// Overridden checks the override level.
+func (TrustedTypesPolicy) Overridden(cfg safehttp.InterceptorConfig) (disabled, reportOnly bool) {
+	disable := cfg.(internalunsafecsp.DisableTrustedTypes)
+	return disable.SkipReports, true
+}
diff --git a/safehttp/plugins/fetchmetadata/fetchmetadata.go b/safehttp/plugins/fetchmetadata/fetchmetadata.go
index d8006f5..cbed936 100644
--- a/safehttp/plugins/fetchmetadata/fetchmetadata.go
+++ b/safehttp/plugins/fetchmetadata/fetchmetadata.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// 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.
@@ -12,174 +12,66 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package fetchmetadata provides a safehttp.Interceptor that applies Fetch
-// Metadata policies to incoming requests in order to protect applications
-// against cross-origin attacks.
+// Package fetchmetadata provides Fetch-Metadata based protections.
 package fetchmetadata
 
 import (
+	"log"
+
 	"github.com/google/go-safeweb/safehttp"
 )
 
-// RequestLogger is used for logging Fetch Metadata policy violations;
-type RequestLogger interface {
-	Log(ir *safehttp.IncomingRequest, navigationIsolation bool)
-}
-
 var (
 	navigationalModes = map[string]bool{
 		"navigate":        true,
 		"nested-navigate": true,
 	}
-	navigationalDest = map[string]bool{
-		"document":        true,
-		"nested-document": true,
-	}
-	statePreservingMethods = map[string]bool{
-		safehttp.MethodGet:  true,
-		safehttp.MethodHead: true,
-	}
 )
 
-// Interceptor implements Fetch Metadata functionality.
-// A pointer to the zero value for Interceptor is safe and ready to use.
-//
-// See https://www.w3.org/TR/fetch-metadata/ and
-// https://web.dev/fetch-metadata/  for more details.
-type Interceptor struct {
-	// NavIsolation indicates whether the Navigation Isolation Policy should
-	// be applied to the request before the Resource Isolation Policy as an
-	// additional layer of checking. This provides a way to mitigate
-	// clickjacking and reflected XSS by rejecting all cross-site navigations
-	// unless targeted to endpoints that are CORS-protected.
-	//
-	// WARNING: This is still an experimental feature and is disabled by
-	// default.
-	NavIsolation bool
-	// RedirectURL can optionally indicate an URL to which the user can be
-	// redirected in case the Navigation Isolation policy rejects the request.
-	RedirectURL *safehttp.URL
-	Logger      RequestLogger
-	reportOnly  bool
-}
-
-var _ safehttp.Interceptor = &Interceptor{}
-
-func (p *Interceptor) checkResourceIsolationPolicy(r *safehttp.IncomingRequest) bool {
-	h := r.Header
-	if h.Get("Sec-Fetch-Site") != "cross-site" {
-		// The request is allowed to pass because one of the following applies:
-		// - Fetch Metadata is not supported by the browser
-		// - the request is same-origin, same-site or caused by the user
-		// explicitly interacting with the user-agent
-		return true
-	}
-
-	method := r.Method()
-	mode := h.Get("Sec-Fetch-Mode")
-	dest := h.Get("Sec-Fetch-Dest")
-	// Allow CORS options requests if neither Mode nor Dest is set.
-	// https://github.com/w3c/webappsec-fetch-metadata/issues/35
-	// https://bugs.chromium.org/p/chromium/issues/detail?id=979946
-	if mode == "" && dest == "" && method == safehttp.MethodOptions {
-		return true
-	}
-
-	if navigationalModes[mode] && navigationalDest[dest] && statePreservingMethods[method] {
-		// The request is cross-site, but a simple top-level navigation from a
-		// safe destination so we  allow it to pass.
-		return true
-	}
-	// The request is cross-site and not a simple navigation or from an unsafe
-	// destination so it is rejected.
-	return false
-}
-
-func (p *Interceptor) checkNavigationIsolationPolicy(r *safehttp.IncomingRequest) bool {
-	if h := r.Header; p.NavIsolation && h.Get("Sec-Fetch-Site") == "cross-site" && navigationalModes[h.Get("Sec-Fetch-Mode")] {
-		return false
-	}
-	return true
-}
-
-// SetReportOnly sets the Fetch Metadata policy mode to "report". This will
-// allow requests that violate the policy to pass, but will report the violation
-// using the Logger. The method will panic if Logger is nil.
-func (p *Interceptor) SetReportOnly() {
-	if p.Logger == nil {
-		panic("logging service required for Fetch Metadata report mode")
-	}
-	p.reportOnly = true
-}
+// TODO(empijei): implement NIP as soon as it's production ready.
 
-// SetEnforce sets the Fetch Metadata policy mode to "enforce". This will reject
-// any requests that violates the policy provided by the plugin.
-func (p *Interceptor) SetEnforce() {
-	p.reportOnly = false
+// Policy is a security policy based on Fetch Metadata.
+//
+// See https://web.dev/fetch-metadata/ for more.
+type Policy struct {
+	isAllowed  func(*safehttp.IncomingRequest) bool
+	match      func(safehttp.InterceptorConfig) bool
+	skip       func(cfg safehttp.InterceptorConfig) (skip, skipReports bool)
+	signal     string
+	navigate   *safehttp.URL
+	ReportOnly bool
 }
 
-// Before validates the safehttp.IncomingRequest using the Resource Isolation
-// Policy and, if enabled, the Navigation Isolation Policy. It only allows
-// requests to pass if they conform to the policy, if it's targeted to one of
-// the CORS-protected endpoints, specified when creating the plugin, or if the
-// mode is set to  "report", in which case the request is allowed to pass but
-// the  violation is reported. If a redirectURL was provided and the Navigation
-// Isolation Policy is enabled and fails, the IncomingRequest will be
-// redirected to redirectURL.
-func (p *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result {
-	rejected := false
-	nav := false
-
-	if !p.checkNavigationIsolationPolicy(r) {
-		rejected = true
-		nav = true
-	}
-
-	if !p.checkResourceIsolationPolicy(r) {
-		rejected = true
+// Before implements the Fetch Metadata validation and signals logic.
+func (p *Policy) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result {
+	skip, skipReports := p.skip(cfg)
+	if p.ReportOnly {
+		skip = true
 	}
 
-	if !rejected {
-		// Both Navigation Isolation and Resource Isolation are happy.
+	if p.isAllowed(r) {
 		return safehttp.NotWritten()
 	}
 
-	// Overridden to be disabled.
-	if d, ok := cfg.(disable); ok {
-		if !d.skipReporting && p.Logger != nil {
-			p.Logger.Log(r, nav)
-		}
-		return safehttp.NotWritten()
+	if !skipReports {
+		// TODO(empijei): report this.
+		log.Printf("Request for %s %q should be blocked by %s. Actually_blocked=%v", r.Method(), r.URL().String(), p.signal, !skip)
 	}
-
-	if p.Logger != nil {
-		p.Logger.Log(r, nav)
-	}
-	if p.reportOnly {
+	if skip {
 		return safehttp.NotWritten()
 	}
-	if nav && p.RedirectURL != nil {
-		return safehttp.Redirect(w, r, p.RedirectURL.String(), safehttp.StatusSeeOther)
+	if safehttp.IsLocalDev() {
+		log.Println("fetchmetadata plugin blocked a potentially malicious request")
+	}
+	if p.navigate != nil {
+		return safehttp.Redirect(w, r, p.navigate.String(), safehttp.StatusSeeOther)
 	}
 	return w.WriteError(safehttp.StatusForbidden)
 }
 
 // Commit is a no-op, required to satisfy the safehttp.Interceptor interface.
-func (p *Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) {
+func (p *Policy) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) {
 }
 
 // Match recongnizes configs to disable fetch metadata protection.
-func (*Interceptor) Match(cfg safehttp.InterceptorConfig) bool {
-	_, ok := cfg.(disable)
-	return ok
-}
-
-type disable struct {
-	skipReporting bool
-}
-
-// Disable returns a configuration that disables Fetch Metadata protection and switches it to report-only.
-// If skipReporting is true, it will also disable reporting.
-func Disable(reason string, skipReporting bool) safehttp.InterceptorConfig {
-	return disable{skipReporting: skipReporting}
-}
+func (p *Policy) Match(cfg safehttp.InterceptorConfig) bool { return p.match(cfg) }
diff --git a/safehttp/plugins/fetchmetadata/framing.go b/safehttp/plugins/fetchmetadata/framing.go
new file mode 100644
index 0000000..9d716f6
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/framing.go
@@ -0,0 +1,83 @@
+// 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 fetchmetadata
+
+import (
+	"log"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+)
+
+var (
+	frameableDests = map[string]bool{
+		"frame":  true,
+		"iframe": true,
+		"embed":  true,
+		"object": true,
+	}
+)
+
+// FramingIsolationPolicy protects from framing attacks.
+//
+// See https://xsleaks.dev/docs/defenses/isolation-policies/framing-isolation/#implementation-with-fetch-metadata
+func FramingIsolationPolicy() *Policy {
+	return &Policy{
+		isAllowed: func(r *safehttp.IncomingRequest) bool {
+			h := r.Header
+			mode := h.Get("Sec-Fetch-Mode")
+			dest := h.Get("Sec-Fetch-Dest")
+			site := h.Get("Sec-Fetch-Site")
+			if mode == "" || dest == "" || site == "" {
+				return true
+			}
+			if !navigationalModes[mode] {
+				// Allow non-navigational requests.
+				return true
+			}
+			if !frameableDests[dest] {
+				// Allow non-frameable requests.
+				return true
+			}
+			if site == "same-origin" || site == "none" {
+				return true
+			}
+			if safehttp.IsLocalDev() {
+				log.Println("fetchmetadata plugin framing protection detected a potentially malicious request")
+			}
+			return false
+		},
+		match: func(cfg safehttp.InterceptorConfig) bool {
+			switch cfg.(type) {
+			case internalunsafeframing.Disable, internalunsafeframing.AllowList:
+				return true
+			}
+			return false
+		},
+		skip: func(cfg safehttp.InterceptorConfig) (skip, skipReports bool) {
+			switch c := cfg.(type) {
+			case internalunsafeframing.Disable:
+				return true, c.SkipReports
+			case internalunsafeframing.AllowList:
+				// It is intended to get someone to frame us.
+				// Since Fetch Metadata is not fine-grained we cannot tell if
+				// a violation is intended, so we should just disable this completely.
+				return true, true
+			}
+			return false, false
+		},
+		signal: "FRAMING_ISOLATION",
+	}
+}
diff --git a/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/internalunsafefetchmetadata.go b/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/internalunsafefetchmetadata.go
new file mode 100644
index 0000000..7dd7489
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/internalunsafefetchmetadata.go
@@ -0,0 +1,22 @@
+// 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 internalunsafefetchmetadata is used internally to override FM policies.
+package internalunsafefetchmetadata
+
+// DisableResourceIsolationPolicy switches RIP to report-only.
+type DisableResourceIsolationPolicy struct {
+	// SkipReports can be used to completely disable RIP.
+	SkipReports bool
+}
diff --git a/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsaferesourcepolicy/unsaferesourcepolicy.go b/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsaferesourcepolicy/unsaferesourcepolicy.go
new file mode 100644
index 0000000..f65d139
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsaferesourcepolicy/unsaferesourcepolicy.go
@@ -0,0 +1,31 @@
+// 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 unsaferesourcepolicy can be used to disable Fetch Metadata protections on specific handler registration.
+//
+// This package is visibility restricted and its uses require a review by the security team.
+package unsaferesourcepolicy
+
+import (
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata"
+)
+
+// Disable switches the resource isolation policy to report-only.
+// If skipReports is true, it will completely disable it.
+func Disable(reason string, skipReports bool) internalunsafefetchmetadata.DisableResourceIsolationPolicy {
+	if reason == "" {
+		panic("reason cannot be empty")
+	}
+	return internalunsafefetchmetadata.DisableResourceIsolationPolicy{SkipReports: skipReports}
+}
diff --git a/safehttp/plugins/fetchmetadata/resource.go b/safehttp/plugins/fetchmetadata/resource.go
new file mode 100644
index 0000000..9aa1827
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/resource.go
@@ -0,0 +1,82 @@
+// 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 fetchmetadata
+
+import (
+	"log"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata"
+)
+
+var (
+	navigationalDests = map[string]bool{
+		"document":        true,
+		"nested-document": true,
+	}
+	statePreservingMethods = map[string]bool{
+		safehttp.MethodGet:  true,
+		safehttp.MethodHead: true,
+	}
+)
+
+// ResourceIsolationPolicy protects resources.
+//
+// See https://web.dev/fetch-metadata/ for more details.
+func ResourceIsolationPolicy() *Policy {
+	return &Policy{
+		isAllowed: func(r *safehttp.IncomingRequest) bool {
+			h := r.Header
+			if h.Get("Sec-Fetch-Site") != "cross-site" {
+				// The request is allowed to pass because one of the following applies:
+				// - Fetch Metadata is not supported by the browser
+				// - the request is same-origin, same-site or caused by the user
+				// explicitly interacting with the user-agent
+				return true
+			}
+
+			method := r.Method()
+			mode := h.Get("Sec-Fetch-Mode")
+			dest := h.Get("Sec-Fetch-Dest")
+			// Allow CORS options requests if neither Mode nor Dest is set.
+			// https://github.com/w3c/webappsec-fetch-metadata/issues/35
+			// https://bugs.chromium.org/p/chromium/issues/detail?id=979946
+			if mode == "" && dest == "" && method == safehttp.MethodOptions {
+				return true
+			}
+
+			if navigationalModes[mode] && navigationalDests[dest] && statePreservingMethods[method] {
+				// The request is cross-site, but a simple top-level navigation from a
+				// safe destination so we allow it to pass.
+				return true
+			}
+			if safehttp.IsLocalDev() {
+				log.Println("fetchmetadata plugin resource protection detected a potentially malicious request")
+			}
+			return false
+		},
+		match: func(cfg safehttp.InterceptorConfig) bool {
+			_, ok := cfg.(internalunsafefetchmetadata.DisableResourceIsolationPolicy)
+			return ok
+		},
+		skip: func(cfg safehttp.InterceptorConfig) (skip, skipReports bool) {
+			if override, ok := cfg.(internalunsafefetchmetadata.DisableResourceIsolationPolicy); ok {
+				return true, override.SkipReports
+			}
+			return false, false
+		},
+		signal: "RESOURCE_ISOLATION",
+	}
+}
diff --git a/safehttp/plugins/framing/framing.go b/safehttp/plugins/framing/framing.go
new file mode 100644
index 0000000..c8a6c46
--- /dev/null
+++ b/safehttp/plugins/framing/framing.go
@@ -0,0 +1,59 @@
+// 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 framing provides utilities to install a comprehensive framing protection.
+package framing
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp"
+
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+)
+
+// Interceptors returns all interceptors needed for a comprehensive framing protection.
+func Interceptors(reportURI string) []safehttp.Interceptor {
+	return []safehttp.Interceptor{
+		fetchmetadata.FramingIsolationPolicy(),
+		csp.Interceptor{Policy: csp.FramingPolicy{ReportURI: reportURI}},
+		xfoInterceptor{},
+	}
+}
+
+type xfoInterceptor struct{}
+
+func (xfoInterceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result {
+	xfo := w.Header().Claim("X-Frame-Options")
+	switch cfg.(type) {
+	case internalunsafeframing.Disable, internalunsafeframing.AllowList:
+		// X-Frame-Options doesn't support allowlists.
+		// We rely on CSP to do the restriction on this value.
+		xfo([]string{"ALLOWALL"})
+	default:
+		xfo([]string{"SAMEORIGIN"})
+	}
+	return safehttp.NotWritten()
+}
+
+func (xfoInterceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg safehttp.InterceptorConfig) {
+}
+
+func (xfoInterceptor) Match(cfg safehttp.InterceptorConfig) bool {
+	switch cfg.(type) {
+	case internalunsafeframing.Disable, internalunsafeframing.AllowList:
+		return true
+	}
+	return false
+}
diff --git a/safehttp/plugins/framing/internalunsafeframing/internalunsafeframing.go b/safehttp/plugins/framing/internalunsafeframing/internalunsafeframing.go
new file mode 100644
index 0000000..d4a0173
--- /dev/null
+++ b/safehttp/plugins/framing/internalunsafeframing/internalunsafeframing.go
@@ -0,0 +1,34 @@
+// 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 internalunsafeframing is used internally to override Framing protections.
+package internalunsafeframing
+
+// Disable turns framing protection to report-only where possible.
+type Disable struct {
+	// SkipReports completely disables framing protectcion.
+	SkipReports bool
+}
+
+// AllowList selectively allows framing.
+//
+// Please note that on older browsers this is equivalent to Disable.
+type AllowList struct {
+	// ReportOnly sets the policy to Report-Only instead of enforcing.
+	ReportOnly bool
+	// Hostnames is a list of origins (with potential wildcards) that will be able to frame the site.
+	// Wildcards must follow the CSP specification:
+	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors.
+	Hostnames []string
+}
diff --git a/safehttp/plugins/framing/internalunsafeframing/unsafeframing/unsafeframing.go b/safehttp/plugins/framing/internalunsafeframing/unsafeframing/unsafeframing.go
new file mode 100644
index 0000000..209ab88
--- /dev/null
+++ b/safehttp/plugins/framing/internalunsafeframing/unsafeframing/unsafeframing.go
@@ -0,0 +1,52 @@
+// 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 unsafeframing can be used to disable Framing protections on specific handler registration.
+//
+// Usage of this package should require a security review.
+package unsafeframing
+
+import (
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+)
+
+// Disable turns framing protections to report-only where supported, otherwise turns them off.
+// If skipReports is true, all protections will be turned completely off.
+func Disable(reason string, skipReports bool) internalunsafeframing.Disable {
+	if reason == "" {
+		panic("reason cannot be empty")
+	}
+	return internalunsafeframing.Disable{SkipReports: skipReports}
+}
+
+// Allow permits to specify a set of hostnames (with potential wildcards) that will be able to frame the site.
+//
+// Wildcards must follow the CSP specification:
+// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors.
+//
+// If reportOnly is true the policy will be set to Report-Only, which provides not security benefit
+// but can be used to detect potential breakages.
+//
+// Please note that this option is only supported by browsers that support CSP: older browsers
+// will end up allowing all origins to frame the site.
+// See support table here: https://caniuse.com/mdn-http_headers_csp_content-security-policy_frame-ancestors.
+func Allow(reason string, reportOnly bool, hostnames ...string) internalunsafeframing.AllowList {
+	if reason == "" {
+		panic("reason cannot be empty")
+	}
+	return internalunsafeframing.AllowList{
+		ReportOnly: reportOnly,
+		Hostnames:  hostnames,
+	}
+}


Test patch:
diff --git a/safehttp/plugins/csp/csp_test.go b/safehttp/plugins/csp/csp_all_test.go
similarity index 63%
rename from safehttp/plugins/csp/csp_test.go
rename to safehttp/plugins/csp/csp_all_test.go
index ef1bf9d..d483bf8 100644
--- a/safehttp/plugins/csp/csp_test.go
+++ b/safehttp/plugins/csp/csp_all_test.go
@@ -15,27 +15,23 @@
 package csp
 
 import (
-	"errors"
 	"os"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp/unsafecspfortests"
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp/unsafestrictcsp"
+	"github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp/unsafetrustedtypes"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing/unsafeframing"
 	"github.com/google/go-safeweb/safehttp/safehttptest"
 )
 
-type endlessAReader struct{}
-
-func (endlessAReader) Read(b []byte) (int, error) {
-	for i := range b {
-		b[i] = 41
-	}
-	return len(b), nil
-}
-
 func TestMain(m *testing.M) {
-	randReader = endlessAReader{}
+	unsafecspfortests.UseStaticRandom()
 	os.Exit(m.Run())
 }
 
@@ -95,21 +91,6 @@ func TestSerialize(t *testing.T) {
 			policy:     FramingPolicy{ReportURI: "httsp://example.com/collector"},
 			wantString: "frame-ancestors 'self'; report-uri httsp://example.com/collector;",
 		},
-		{
-			name: "FramingCSP with one source",
-			policy: FramingPolicy{Sources: []string{
-				"https://www.example.org",
-			}},
-			wantString: "frame-ancestors 'self' https://www.example.org;",
-		},
-		{
-			name: "FramingCSP with multiple sources",
-			policy: FramingPolicy{Sources: []string{
-				"https://a.example.org",
-				"https://b.example.org",
-			}},
-			wantString: "frame-ancestors 'self' https://a.example.org https://b.example.org;",
-		},
 		{
 			name:       "TrustedTypesCSP",
 			policy:     TrustedTypesPolicy{},
@@ -124,7 +105,7 @@ func TestSerialize(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			s := tt.policy.Serialize("super-secret")
+			s := tt.policy.Serialize("super-secret", nil)
 
 			if s != tt.wantString {
 				t.Errorf("tt.policy.Serialize() got: %q want: %q", s, tt.wantString)
@@ -136,43 +117,47 @@ func TestSerialize(t *testing.T) {
 func TestBefore(t *testing.T) {
 	tests := []struct {
 		name                 string
-		interceptor          Interceptor
+		interceptors         []Interceptor
 		wantEnforcePolicy    []string
 		wantReportOnlyPolicy []string
 		wantNonce            string
 	}{
 		{
-			name:        "No policies",
-			interceptor: Interceptor{},
-			wantNonce:   "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
+			name:         "Default policies",
+			interceptors: Default(""),
+			wantEnforcePolicy: []string{
+				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'",
+				"require-trusted-types-for 'script'",
+			},
+			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
 		},
 		{
-			name:        "Default policies",
-			interceptor: Default(""),
+			name:         "All policies",
+			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
 			wantEnforcePolicy: []string{
 				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'",
-				"frame-ancestors 'self';",
 				"require-trusted-types-for 'script'",
+				"frame-ancestors 'self';",
 			},
 			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
 		},
 		{
-			name:        "Default policies with reporting URI",
-			interceptor: Default("https://example.com/collector"),
+			name: "All policies with reporting URI",
+			interceptors: append(Default("https://example.com/collector"),
+				Interceptor{Policy: FramingPolicy{ReportURI: "https://example.com/collector"}}),
 			wantEnforcePolicy: []string{
 				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'; report-uri https://example.com/collector",
-				"frame-ancestors 'self'; report-uri https://example.com/collector;",
 				"require-trusted-types-for 'script'; report-uri https://example.com/collector",
+				"frame-ancestors 'self'; report-uri https://example.com/collector;",
 			},
 			wantNonce: "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
 		},
 		{
 			name: "StrictCSP Report Only",
-			interceptor: Interceptor{
-				ReportOnly: []Policy{
-					StrictPolicy{ReportURI: "https://example.com/collector"},
-				},
-			},
+			interceptors: []Interceptor{{
+				Policy:     StrictPolicy{ReportURI: "https://example.com/collector"},
+				ReportOnly: true,
+			}},
 			wantReportOnlyPolicy: []string{
 				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'; report-uri https://example.com/collector",
 			},
@@ -180,11 +165,10 @@ func TestBefore(t *testing.T) {
 		},
 		{
 			name: "FramingCSP Report Only",
-			interceptor: Interceptor{
-				ReportOnly: []Policy{
-					FramingPolicy{ReportURI: "https://example.com/collector"},
-				},
-			},
+			interceptors: []Interceptor{{
+				Policy:     FramingPolicy{ReportURI: "https://example.com/collector"},
+				ReportOnly: true,
+			}},
 			wantReportOnlyPolicy: []string{"frame-ancestors 'self'; report-uri https://example.com/collector;"},
 			wantNonce:            "KSkpKSkpKSkpKSkpKSkpKSkpKSk=",
 		},
@@ -195,7 +179,9 @@ func TestBefore(t *testing.T) {
 			fakeRW, rr := safehttptest.NewFakeResponseWriter()
 			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
 
-			tt.interceptor.Before(fakeRW, req, nil)
+			for _, i := range tt.interceptors {
+				i.Before(fakeRW, req, nil)
+			}
 
 			h := rr.Header()
 			if diff := cmp.Diff(tt.wantEnforcePolicy, h.Values("Content-Security-Policy"), cmpopts.EquateEmpty()); diff != "" {
@@ -206,9 +192,9 @@ func TestBefore(t *testing.T) {
 				t.Errorf("h.Values(\"Content-Security-Policy-Report-Only\") mismatch (-want +got):\n%s", diff)
 			}
 
-			v := safehttp.FlightValues(req.Context()).Get(nonceKey{})
+			v := safehttp.FlightValues(req.Context()).Get(nonceKey)
 			if v == nil {
-				t.Fatalf("FlightValues(...).Get(nonceKey{}) got: nil want: %q", tt.wantNonce)
+				t.Fatalf("safehttp.FlightValues(req.Context()).Get(nonceCtxKey) got: nil want: %q", tt.wantNonce)
 			}
 			if got := v.(string); got != tt.wantNonce {
 				t.Errorf("v.(string) got: %q want: %q", got, tt.wantNonce)
@@ -217,43 +203,24 @@ func TestBefore(t *testing.T) {
 	}
 }
 
-type errorReader struct{}
-
-func (errorReader) Read(b []byte) (int, error) {
-	return 0, errors.New("bad")
-}
-
-func TestPanicWhileGeneratingNonce(t *testing.T) {
-	randReader = errorReader{}
-	defer func() {
-		// TODO: avoid replacing the randReader per individual test cases
-		randReader = endlessAReader{}
-	}()
-	defer func() {
-		if r := recover(); r != nil {
-			return
-		}
-		t.Error("generateNonce() expected panic")
-	}()
-	generateNonce()
-}
-
 func TestValidNonce(t *testing.T) {
 	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
-	safehttp.FlightValues(req.Context()).Put(nonceKey{}, "nonce")
+	_ = nonce(req)
+
 	n, err := Nonce(req.Context())
 	if err != nil {
 		t.Errorf("Nonce(ctx) got err: %v want: nil", err)
 	}
 
-	if want := "nonce"; n != want {
+	if want := "KSkpKSkpKSkpKSkpKSkpKSkpKSk="; n != want {
 		t.Errorf("Nonce(ctx) got nonce: %v want: %v", n, want)
 	}
 }
 
 func TestNonceEmptyContext(t *testing.T) {
 	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
-	safehttp.FlightValues(req.Context()) // not putting nonce
+	// Not using nonce() to insert the nonce in context.
+
 	n, err := Nonce(req.Context())
 	if err == nil {
 		t.Error("Nonce(ctx) got err: nil want: error")
@@ -267,7 +234,7 @@ func TestNonceEmptyContext(t *testing.T) {
 func TestCommitNonce(t *testing.T) {
 	fakeRW, rr := safehttptest.NewFakeResponseWriter()
 	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
-	safehttp.FlightValues(req.Context()).Put(nonceKey{}, "pizza")
+	safehttp.FlightValues(req.Context()).Put(nonceKey, "pizza")
 
 	it := Interceptor{}
 	tr := &safehttp.TemplateResponse{}
@@ -302,7 +269,7 @@ func TestCommitNonce(t *testing.T) {
 func TestCommitMissingNonce(t *testing.T) {
 	fakeRW, _ := safehttptest.NewFakeResponseWriter()
 	req := safehttptest.NewRequest(safehttp.MethodGet, "https://foo.com/pizza", nil)
-	safehttp.FlightValues(req.Context()).Put(nonceKey{}, nil /* missing nonce */)
+	// Not adding safehttp.FlightValues here.
 
 	it := Interceptor{}
 	tr := &safehttp.TemplateResponse{}
@@ -335,3 +302,94 @@ func TestCommitNotTemplateResponse(t *testing.T) {
 	}
 
 }
+
+func TestOverride(t *testing.T) {
+	tests := []struct {
+		name                 string
+		interceptors         []Interceptor
+		overrides            []safehttp.InterceptorConfig
+		wantEnforcePolicy    []string
+		wantReportOnlyPolicy []string
+	}{
+		{
+			name:         "All policies, completely disabled",
+			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
+			overrides: []safehttp.InterceptorConfig{
+				internalunsafecsp.DisableStrict{SkipReports: true},
+				internalunsafecsp.DisableTrustedTypes{SkipReports: true},
+				internalunsafeframing.Disable{SkipReports: true},
+			},
+		},
+		{
+			name:         "All policies, disabled via unsafe packages",
+			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
+			overrides: []safehttp.InterceptorConfig{
+				unsafestrictcsp.Disable("testing", true),
+				unsafetrustedtypes.Disable("testing", true),
+				unsafeframing.Disable("testing", true),
+			},
+		},
+		{
+			name:         "All policies, report-only override",
+			interceptors: append(Default(""), Interceptor{Policy: FramingPolicy{}}),
+			overrides: []safehttp.InterceptorConfig{
+				internalunsafecsp.DisableStrict{},
+				internalunsafecsp.DisableTrustedTypes{},
+				internalunsafeframing.Disable{},
+			},
+			wantReportOnlyPolicy: []string{
+				"object-src 'none'; script-src 'unsafe-inline' 'nonce-KSkpKSkpKSkpKSkpKSkpKSkpKSk=' 'strict-dynamic' https: http:; base-uri 'none'",
+				"require-trusted-types-for 'script'",
+				"frame-ancestors 'self';",
+			},
+		},
+		{
+			name: "FramingCSP allowlist",
+			interceptors: []Interceptor{Interceptor{
+				Policy: FramingPolicy{}}},
+			overrides: []safehttp.InterceptorConfig{
+				unsafeframing.Allow("testing", true, "https://www.example.org"),
+			},
+			wantReportOnlyPolicy: []string{"frame-ancestors 'self' https://www.example.org;"},
+		},
+		{
+			name: "FramingCSP allowlist",
+			interceptors: []Interceptor{Interceptor{
+				Policy: FramingPolicy{}}},
+			overrides: []safehttp.InterceptorConfig{
+				unsafeframing.Allow("testing", false, "https://a.example.org", "https://b.example.org"),
+			},
+			wantEnforcePolicy: []string{
+				"frame-ancestors 'self' https://a.example.org https://b.example.org;"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+
+			for _, i := range tt.interceptors {
+				var cfg safehttp.InterceptorConfig
+				for _, c := range tt.overrides {
+					if i.Match(c) {
+						if cfg != nil {
+							t.Fatalf("Multiple overrides match: %v and %v", cfg, c)
+						}
+						cfg = c
+					}
+				}
+				i.Before(fakeRW, req, cfg)
+			}
+
+			h := rr.Header()
+			if diff := cmp.Diff(tt.wantEnforcePolicy, h.Values("Content-Security-Policy"), cmpopts.EquateEmpty()); diff != "" {
+				t.Errorf("h.Values(\"Content-Security-Policy\") mismatch (-want +got):\n%s", diff)
+			}
+
+			if diff := cmp.Diff(tt.wantReportOnlyPolicy, h.Values("Content-Security-Policy-Report-Only"), cmpopts.EquateEmpty()); diff != "" {
+				t.Errorf("h.Values(\"Content-Security-Policy-Report-Only\") mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/safehttp/plugins/csp/internalunsafecsp/unsafecspfortests/unsafecspfortests.go b/safehttp/plugins/csp/internalunsafecsp/unsafecspfortests/unsafecspfortests.go
new file mode 100644
index 0000000..b2740b1
--- /dev/null
+++ b/safehttp/plugins/csp/internalunsafecsp/unsafecspfortests/unsafecspfortests.go
@@ -0,0 +1,44 @@
+// 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 unsafecspfortests can be used to disable CSP on specific handler registration in tests.
+//
+// This package should only be used in tests.
+package unsafecspfortests
+
+import "github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp"
+
+type endlessAReader struct{}
+
+func (endlessAReader) Read(b []byte) (int, error) {
+	for i := range b {
+		b[i] = 41
+	}
+	return len(b), nil
+}
+
+// UseStaticRandom will make all nonces assume a static value.
+func UseStaticRandom() {
+	internalunsafecsp.RandReader = endlessAReader{}
+}
+
+// DisableStrict completely disables Strict CSP.
+func DisableStrict() internalunsafecsp.DisableStrict {
+	return internalunsafecsp.DisableStrict{SkipReports: true}
+}
+
+// DisableTrustedTypes completely disables TrustedTypes
+func DisableTrustedTypes() internalunsafecsp.DisableTrustedTypes {
+	return internalunsafecsp.DisableTrustedTypes{SkipReports: true}
+}
diff --git a/safehttp/plugins/fetchmetadata/fetchmetadata_test.go b/safehttp/plugins/fetchmetadata/fetchmetadata_test.go
index 4ade959..e6ed4fa 100644
--- a/safehttp/plugins/fetchmetadata/fetchmetadata_test.go
+++ b/safehttp/plugins/fetchmetadata/fetchmetadata_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// 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.
@@ -15,11 +15,13 @@
 package fetchmetadata_test
 
 import (
+	"net/http"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-safeweb/safehttp"
 	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+
+	"github.com/google/go-safeweb/safehttp"
 	"github.com/google/go-safeweb/safehttp/safehttptest"
 )
 
@@ -27,495 +29,50 @@ type testHeaders struct {
 	name, method, site, mode, dest string
 }
 
-var (
-	allowedRIPHeaders = []testHeaders{
-		{
-			name:   "Fetch Metadata not supported",
-			method: safehttp.MethodGet,
-			site:   "",
-		},
-		{
-			name:   "same origin",
-			method: safehttp.MethodGet,
-			site:   "same-origin",
-		},
-		{
-			name:   "same site",
-			method: safehttp.MethodGet,
-			site:   "same-site",
-		},
-		{
-			name:   "user agent initiated",
-			method: safehttp.MethodGet,
-			site:   "none",
-		},
-		{
-			name:   "cors bug missing mode",
-			method: safehttp.MethodOptions,
-			site:   "cross-site",
-			mode:   "",
-		},
-	}
-
-	allowedRIPNavHeaders = []testHeaders{
-		{
-			name:   "cross origin GET navigate from document",
-			method: safehttp.MethodGet,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "document",
-		},
-		{
-			name:   "cross origin HEAD navigate from document",
-			method: safehttp.MethodHead,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "document",
-		},
-		{
-			name:   "cross origin GET navigate from nested-document",
-			method: safehttp.MethodGet,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "nested-document",
-		},
-		{
-			name:   "cross origin HEAD navigate from nested-document",
-			method: safehttp.MethodHead,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "nested-document",
-		},
-		{
-			name:   "cross origin GET nested-navigate from document",
-			method: safehttp.MethodGet,
-			site:   "cross-site",
-			mode:   "nested-navigate",
-			dest:   "document",
-		},
-		{
-			name:   "cross origin HEAD nested-navigate from document",
-			method: safehttp.MethodHead,
-			site:   "cross-site",
-			mode:   "nested-navigate",
-			dest:   "document",
-		},
-		{
-			name:   "cross origin GET nested-navigate from nested-document",
-			method: safehttp.MethodGet,
-			site:   "cross-site",
-			mode:   "nested-navigate",
-			dest:   "nested-document",
-		},
-		{
-			name:   "cross origin HEAD nested-navigate from nested-document",
-			method: safehttp.MethodHead,
-			site:   "cross-site",
-			mode:   "nested-navigate",
-			dest:   "nested-document",
-		},
-	}
-
-	disallowedRIPNavHeaders = []testHeaders{
-		{
-			name:   "cross origin POST",
-			method: safehttp.MethodPost,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "document",
-		},
-		{
-			name:   "cross origin GET from object",
-			method: safehttp.MethodGet,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "object",
-		},
-		{
-			name:   "cross origin HEAD from embed",
-			method: safehttp.MethodHead,
-			site:   "cross-site",
-			mode:   "navigate",
-			dest:   "embed",
-		},
-	}
-
-	disallowedRIPHeaders = []testHeaders{
-		{
-			name:   "cross origin no cors",
-			method: safehttp.MethodPost,
-			site:   "cross-site",
-			mode:   "cors",
-			dest:   "document",
-		},
-		{
-			name:   "cross origin no cors",
-			method: safehttp.MethodPost,
-			site:   "cross-site",
-			mode:   "no-cors",
-			dest:   "nested-document",
-		},
-	}
-)
-
-func TestAllowedResourceIsolationEnforceMode(t *testing.T) {
-	tests := append(allowedRIPHeaders, allowedRIPNavHeaders...)
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			p := &fetchmetadata.Interceptor{}
-			p.Before(fakeRW, req, nil)
-
-			if want, got := int(safehttp.StatusOK), rr.Code; got != want {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-			if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
-				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
-			}
-			if want, got := "", rr.Body.String(); got != want {
-				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
-			}
-		})
-	}
-}
-
-func TestRejectedResourceIsolationEnforceMode(t *testing.T) {
-	tests := append(disallowedRIPHeaders, disallowedRIPNavHeaders...)
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			p := &fetchmetadata.Interceptor{}
-			p.Before(fakeRW, req, nil)
-
-			if want, got := safehttp.StatusForbidden, safehttp.StatusCode(rr.Code); want != got {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-		})
-	}
-}
-
-type reportTests struct {
-	name, method, site, mode, dest, report string
-}
-
-type methodLogger struct {
-	report string
-	nav    bool
-}
-
-func (l *methodLogger) Log(r *safehttp.IncomingRequest, nav bool) {
-	l.report = r.Method()
-	l.nav = nav
-}
-
-func TestRejectedResourceIsolationEnforceModeWithLogger(t *testing.T) {
-	var tests []reportTests
-	for _, t := range disallowedRIPHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
-		})
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			logger := &methodLogger{}
-			p := &fetchmetadata.Interceptor{
-				Logger: logger,
-			}
-			p.Before(fakeRW, req, nil)
-
-			if want, got := safehttp.StatusForbidden, safehttp.StatusCode(rr.Code); want != got {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-			if test.report != logger.report {
-				t.Errorf("logger.report: got %s, want %s", logger.report, test.report)
-			}
-			if logger.nav != false {
-				t.Errorf("logger.nav: got %v, want %v", logger.nav, false)
-			}
-		})
-	}
-}
-
-func TestResourceIsolationReportMode(t *testing.T) {
-	var tests []reportTests
-	for _, t := range allowedRIPHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: "",
-		})
-	}
-	for _, t := range disallowedRIPHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
-		})
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			p := &fetchmetadata.Interceptor{}
-			logger := &methodLogger{}
-			p.Logger = logger
-			p.SetReportOnly()
-			p.Before(fakeRW, req, nil)
-
-			if want, got := safehttp.StatusOK, safehttp.StatusCode(rr.Code); got != want {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-			if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
-				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
-			}
-			if want, got := "", rr.Body.String(); got != want {
-				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
-			}
-			if logger.report != test.report {
-				t.Errorf("logger.report: got %s, want %s", logger.report, test.report)
-			}
-			if logger.nav != false {
-				t.Errorf("logger.nav: got %v, want %v", logger.nav, false)
-			}
-		})
-	}
-}
-
-func TestReportModeMissingLogger(t *testing.T) {
-	p := &fetchmetadata.Interceptor{}
-	defer func() {
-		if r := recover(); r != nil {
-			return
-		}
-		t.Error("p.SetReportOnly(nil) expected panic")
-	}()
-	p.SetReportOnly()
-}
-
-func TestNavIsolationEnforceMode(t *testing.T) {
-	tests := append(allowedRIPNavHeaders, disallowedRIPNavHeaders...)
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			p := &fetchmetadata.Interceptor{
-				NavIsolation: true,
-			}
-			p.Before(fakeRW, req, nil)
-
-			if want, got := safehttp.StatusForbidden, safehttp.StatusCode(rr.Code); want != got {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-		})
-	}
-}
-
-func TestNavIsolationReportMode(t *testing.T) {
+func TestReportOnly(t *testing.T) {
 	type reportTests struct {
-		name, method, site, mode, dest, report string
+		name, site, mode, dest, block string
 	}
 	var tests []reportTests
-	for _, t := range allowedRIPNavHeaders {
+	for _, t := range allowedFIPHeaders {
 		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
+			name:  t.name,
+			site:  t.site,
+			mode:  t.mode,
+			dest:  t.dest,
+			block: "false",
 		})
 	}
-	for _, t := range disallowedRIPNavHeaders {
+	for _, t := range disallowedFIPHeaders {
 		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
+			name:  t.name,
+			site:  t.site,
+			mode:  t.mode,
+			dest:  t.dest,
+			block: "true",
 		})
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
+			req := safehttptest.NewRequest("GET", "https://spaghetti.com/carbonara", nil)
 			req.Header.Add("Sec-Fetch-Site", test.site)
 			req.Header.Add("Sec-Fetch-Mode", test.mode)
 			req.Header.Add("Sec-Fetch-Dest", test.dest)
 			fakeRW, rr := safehttptest.NewFakeResponseWriter()
 
-			logger := &methodLogger{}
-			p := &fetchmetadata.Interceptor{
-				Logger:       logger,
-				NavIsolation: true,
-			}
-			p.SetReportOnly()
+			p := fetchmetadata.FramingIsolationPolicy()
+			p.ReportOnly = true
 			p.Before(fakeRW, req, nil)
 
 			if want, got := safehttp.StatusOK, safehttp.StatusCode(rr.Code); want != got {
 				t.Errorf("rr.Code got: %v want: %v", got, want)
 			}
-			if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
-				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
-			}
-			if want, got := "", rr.Body.String(); got != want {
-				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
-			}
-			if logger.report != test.report {
-				t.Errorf("logger.report: got %s, want %s", logger.report, test.report)
-			}
-			if logger.nav != true {
-				t.Errorf("logger.nav: got %v, want %v", logger.nav, true)
-			}
-		})
-	}
-}
-
-func TestDisable(t *testing.T) {
-	type reportTests struct {
-		name, method, site, mode, dest, report string
-	}
-	var tests []reportTests
-	for _, t := range allowedRIPNavHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
-		})
-	}
-	for _, t := range disallowedRIPNavHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
-		})
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			logger := &methodLogger{}
-			p := &fetchmetadata.Interceptor{
-				Logger:       logger,
-				NavIsolation: true,
-			}
-			p.Before(fakeRW, req, fetchmetadata.Disable("testing", false))
-
-			if want, got := safehttp.StatusOK, safehttp.StatusCode(rr.Code); want != got {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-			if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
+			if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
 				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
 			}
 			if want, got := "", rr.Body.String(); got != want {
 				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
 			}
-			if logger.report != test.report {
-				t.Errorf("logger.report: got %s, want %s", logger.report, test.report)
-			}
-			if logger.nav != true {
-				t.Errorf("logger.nav: got %v, want %v", logger.nav, true)
-			}
-		})
-	}
-}
-
-func TestDisableSkipLogger(t *testing.T) {
-	type reportTests struct {
-		name, method, site, mode, dest, report string
-	}
-	var tests []reportTests
-	for _, t := range allowedRIPNavHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
-		})
-	}
-	for _, t := range disallowedRIPNavHeaders {
-		tests = append(tests, reportTests{
-			name:   t.name,
-			method: t.method,
-			site:   t.site,
-			mode:   t.mode,
-			dest:   t.dest,
-			report: t.method,
-		})
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
-			req.Header.Add("Sec-Fetch-Site", test.site)
-			req.Header.Add("Sec-Fetch-Mode", test.mode)
-			req.Header.Add("Sec-Fetch-Dest", test.dest)
-			fakeRW, rr := safehttptest.NewFakeResponseWriter()
-
-			logger := &methodLogger{}
-			p := &fetchmetadata.Interceptor{
-				Logger:       logger,
-				NavIsolation: true,
-			}
-			p.Before(fakeRW, req, fetchmetadata.Disable("testing", true))
-
-			if want, got := safehttp.StatusOK, safehttp.StatusCode(rr.Code); want != got {
-				t.Errorf("rr.Code got: %v want: %v", got, want)
-			}
-			if diff := cmp.Diff(map[string][]string{}, map[string][]string(rr.Header())); diff != "" {
-				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
-			}
-			if want, got := "", rr.Body.String(); got != want {
-				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
-			}
-			if logger.report != "" {
-				t.Error("logger was called but it shouldn't have been")
-			}
 		})
 	}
 }
diff --git a/safehttp/plugins/fetchmetadata/framing_test.go b/safehttp/plugins/fetchmetadata/framing_test.go
new file mode 100644
index 0000000..acd1dbe
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/framing_test.go
@@ -0,0 +1,176 @@
+// 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 fetchmetadata_test
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+var (
+	allowedFIPHeaders = []testHeaders{
+		{
+			name: "Fetch Metadata not supported",
+		},
+		{
+			name: "Same origin",
+			site: "same-origin",
+			mode: "navigate",
+			dest: "frame",
+		},
+		{
+			name: "User agent initiated",
+			site: "none",
+			mode: "navigate",
+			dest: "frame",
+		},
+		{
+			name: "Non-navigational",
+			site: "cross-site",
+			mode: "cors",
+			dest: "frame",
+		},
+		{
+			name: "Non-frameable",
+			site: "cross-site",
+			mode: "navigate",
+			dest: "script",
+		},
+	}
+	disallowedFIPHeaders = []testHeaders{
+		{
+			name: "Cross origin frame",
+			site: "cross-origin",
+			mode: "navigate",
+			dest: "frame",
+		},
+		{
+			name: "Same site, corss origin embed",
+			site: "same-site",
+			mode: "nested-navigate",
+			dest: "embed",
+		},
+	}
+)
+
+func TestAllowedFramingIsolationEnforceMode(t *testing.T) {
+	for _, test := range allowedFIPHeaders {
+		t.Run(test.name, func(t *testing.T) {
+			req := safehttptest.NewRequest("GET", "https://spaghetti.com/carbonara", nil)
+			req.Header.Add("Sec-Fetch-Site", test.site)
+			req.Header.Add("Sec-Fetch-Mode", test.mode)
+			req.Header.Add("Sec-Fetch-Dest", test.dest)
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+			p := fetchmetadata.FramingIsolationPolicy()
+			p.Before(fakeRW, req, nil)
+
+			if want, got := int(safehttp.StatusOK), rr.Code; got != want {
+				t.Errorf("rr.Code got: %v want: %v", got, want)
+			}
+			if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
+				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+			}
+			if want, got := "", rr.Body.String(); got != want {
+				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
+			}
+		})
+	}
+}
+
+func TestRejectedFramingIsolationEnforceMode(t *testing.T) {
+	for _, test := range disallowedFIPHeaders {
+		t.Run(test.name, func(t *testing.T) {
+			req := safehttptest.NewRequest("GET", "https://spaghetti.com/carbonara", nil)
+			req.Header.Add("Sec-Fetch-Site", test.site)
+			req.Header.Add("Sec-Fetch-Mode", test.mode)
+			req.Header.Add("Sec-Fetch-Dest", test.dest)
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+			p := fetchmetadata.FramingIsolationPolicy()
+			p.Before(fakeRW, req, nil)
+
+			if want, got := safehttp.StatusForbidden, safehttp.StatusCode(rr.Code); want != got {
+				t.Errorf("rr.Code got: %v want: %v", got, want)
+			}
+			if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
+				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestDisableFramingIsolationPolicy(t *testing.T) {
+	type reportTests struct {
+		name, site, mode, dest, block string
+	}
+	var tests []reportTests
+	for _, t := range allowedFIPHeaders {
+		tests = append(tests, reportTests{
+			name:  t.name,
+			site:  t.site,
+			mode:  t.mode,
+			dest:  t.dest,
+			block: "false",
+		})
+	}
+	for _, t := range disallowedFIPHeaders {
+		tests = append(tests, reportTests{
+			name:  t.name,
+			site:  t.site,
+			mode:  t.mode,
+			dest:  t.dest,
+			block: "true",
+		})
+	}
+	overrides := []struct {
+		name  string
+		value safehttp.InterceptorConfig
+	}{
+		{"disable", internalunsafeframing.Disable{SkipReports: true}},
+		{"allowlist", internalunsafeframing.AllowList{}},
+	}
+	for _, override := range overrides {
+		for _, test := range tests {
+			t.Run(test.name+" "+override.name, func(t *testing.T) {
+				req := safehttptest.NewRequest("GET", "https://spaghetti.com/carbonara", nil)
+				req.Header.Add("Sec-Fetch-Site", test.site)
+				req.Header.Add("Sec-Fetch-Mode", test.mode)
+				req.Header.Add("Sec-Fetch-Dest", test.dest)
+				fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+				p := fetchmetadata.FramingIsolationPolicy()
+				p.Before(fakeRW, req, override.value)
+
+				if want, got := safehttp.StatusOK, safehttp.StatusCode(rr.Code); want != got {
+					t.Errorf("rr.Code got: %v want: %v", got, want)
+				}
+				if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
+					t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+				}
+				if want, got := "", rr.Body.String(); got != want {
+					t.Errorf("rr.Body.String() got: %q want: %q", got, want)
+				}
+			})
+		}
+	}
+}
diff --git a/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsafefetchmetadatafortests/unsafefetchmetadatafortests.go b/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsafefetchmetadatafortests/unsafefetchmetadatafortests.go
new file mode 100644
index 0000000..08f2d9b
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsafefetchmetadatafortests/unsafefetchmetadatafortests.go
@@ -0,0 +1,28 @@
+// 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 unsafefetchmetadatafortests can be used to disable Fetch Metadata protections on specific
+// handler registration in tests.
+//
+// This package should only be used in tests.
+package unsafefetchmetadatafortests
+
+import (
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata"
+)
+
+// DisableResourceIsolationPolicy compltetely disables the policy.
+func DisableResourceIsolationPolicy() internalunsafefetchmetadata.DisableResourceIsolationPolicy {
+	return internalunsafefetchmetadata.DisableResourceIsolationPolicy{SkipReports: true}
+}
diff --git a/safehttp/plugins/fetchmetadata/resource_test.go b/safehttp/plugins/fetchmetadata/resource_test.go
new file mode 100644
index 0000000..b75e37f
--- /dev/null
+++ b/safehttp/plugins/fetchmetadata/resource_test.go
@@ -0,0 +1,256 @@
+// 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 fetchmetadata_test
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata"
+	"github.com/google/go-safeweb/safehttp/plugins/fetchmetadata/internalunsafefetchmetadata/unsafefetchmetadatafortests"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+var (
+	allowedRIPHeaders = []testHeaders{
+		{
+			name:   "Fetch Metadata not supported",
+			method: safehttp.MethodGet,
+			site:   "",
+		},
+		{
+			name:   "same origin",
+			method: safehttp.MethodGet,
+			site:   "same-origin",
+		},
+		{
+			name:   "same site",
+			method: safehttp.MethodGet,
+			site:   "same-site",
+		},
+		{
+			name:   "user agent initiated",
+			method: safehttp.MethodGet,
+			site:   "none",
+		},
+		{
+			name:   "cors bug missing mode",
+			method: safehttp.MethodOptions,
+			site:   "cross-site",
+			mode:   "",
+		},
+	}
+
+	allowedRIPNavHeaders = []testHeaders{
+		{
+			name:   "cross origin GET navigate from document",
+			method: safehttp.MethodGet,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "document",
+		},
+		{
+			name:   "cross origin HEAD navigate from document",
+			method: safehttp.MethodHead,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "document",
+		},
+		{
+			name:   "cross origin GET navigate from nested-document",
+			method: safehttp.MethodGet,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "nested-document",
+		},
+		{
+			name:   "cross origin HEAD navigate from nested-document",
+			method: safehttp.MethodHead,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "nested-document",
+		},
+		{
+			name:   "cross origin GET nested-navigate from document",
+			method: safehttp.MethodGet,
+			site:   "cross-site",
+			mode:   "nested-navigate",
+			dest:   "document",
+		},
+		{
+			name:   "cross origin HEAD nested-navigate from document",
+			method: safehttp.MethodHead,
+			site:   "cross-site",
+			mode:   "nested-navigate",
+			dest:   "document",
+		},
+		{
+			name:   "cross origin GET nested-navigate from nested-document",
+			method: safehttp.MethodGet,
+			site:   "cross-site",
+			mode:   "nested-navigate",
+			dest:   "nested-document",
+		},
+		{
+			name:   "cross origin HEAD nested-navigate from nested-document",
+			method: safehttp.MethodHead,
+			site:   "cross-site",
+			mode:   "nested-navigate",
+			dest:   "nested-document",
+		},
+	}
+
+	disallowedRIPNavHeaders = []testHeaders{
+		{
+			name:   "cross origin POST",
+			method: safehttp.MethodPost,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "document",
+		},
+		{
+			name:   "cross origin GET from object",
+			method: safehttp.MethodGet,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "object",
+		},
+		{
+			name:   "cross origin HEAD from embed",
+			method: safehttp.MethodHead,
+			site:   "cross-site",
+			mode:   "navigate",
+			dest:   "embed",
+		},
+	}
+
+	disallowedRIPHeaders = []testHeaders{
+		{
+			name:   "cross origin no cors",
+			method: safehttp.MethodPost,
+			site:   "cross-site",
+			mode:   "cors",
+			dest:   "document",
+		},
+		{
+			name:   "cross origin no cors",
+			method: safehttp.MethodPost,
+			site:   "cross-site",
+			mode:   "no-cors",
+			dest:   "nested-document",
+		},
+	}
+)
+
+func TestAllowedResourceIsolationEnforceMode(t *testing.T) {
+	tests := append(allowedRIPHeaders, allowedRIPNavHeaders...)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
+			req.Header.Add("Sec-Fetch-Site", test.site)
+			req.Header.Add("Sec-Fetch-Mode", test.mode)
+			req.Header.Add("Sec-Fetch-Dest", test.dest)
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+			p := fetchmetadata.ResourceIsolationPolicy()
+			p.Before(fakeRW, req, nil)
+
+			if want, got := int(safehttp.StatusOK), rr.Code; got != want {
+				t.Errorf("rr.Code got: %v want: %v", got, want)
+			}
+			if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
+				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+			}
+			if want, got := "", rr.Body.String(); got != want {
+				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
+			}
+		})
+	}
+}
+
+func TestRejectedResourceIsolationEnforceMode(t *testing.T) {
+	tests := append(disallowedRIPHeaders, disallowedRIPNavHeaders...)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
+			req.Header.Add("Sec-Fetch-Site", test.site)
+			req.Header.Add("Sec-Fetch-Mode", test.mode)
+			req.Header.Add("Sec-Fetch-Dest", test.dest)
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+			p := fetchmetadata.ResourceIsolationPolicy()
+			p.Before(fakeRW, req, nil)
+
+			if want, got := safehttp.StatusForbidden, safehttp.StatusCode(rr.Code); want != got {
+				t.Errorf("rr.Code got: %v want: %v", got, want)
+			}
+			if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
+				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestDisableResourceIsolationPolicy(t *testing.T) {
+	type reportTests struct {
+		name, method, site, mode, dest, blocked string
+	}
+	var tests []reportTests
+	for _, t := range append(allowedRIPNavHeaders, allowedRIPHeaders...) {
+		tests = append(tests, reportTests{
+			name:    t.name,
+			method:  t.method,
+			site:    t.site,
+			mode:    t.mode,
+			dest:    t.dest,
+			blocked: "false",
+		})
+	}
+	for _, t := range append(disallowedRIPNavHeaders, disallowedRIPHeaders...) {
+		tests = append(tests, reportTests{
+			name:    t.name,
+			method:  t.method,
+			site:    t.site,
+			mode:    t.mode,
+			dest:    t.dest,
+			blocked: "true",
+		})
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := safehttptest.NewRequest(test.method, "https://spaghetti.com/carbonara", nil)
+			req.Header.Add("Sec-Fetch-Site", test.site)
+			req.Header.Add("Sec-Fetch-Mode", test.mode)
+			req.Header.Add("Sec-Fetch-Dest", test.dest)
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+			p := fetchmetadata.ResourceIsolationPolicy()
+			p.Before(fakeRW, req, unsafefetchmetadatafortests.DisableResourceIsolationPolicy())
+
+			if want, got := safehttp.StatusOK, safehttp.StatusCode(rr.Code); want != got {
+				t.Errorf("rr.Code got: %v want: %v", got, want)
+			}
+			if diff := cmp.Diff(http.Header{}, rr.Header()); diff != "" {
+				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+			}
+			if want, got := "", rr.Body.String(); got != want {
+				t.Errorf("rr.Body.String() got: %q want: %q", got, want)
+			}
+		})
+	}
+}
diff --git a/safehttp/plugins/framing/framing_test.go b/safehttp/plugins/framing/framing_test.go
new file mode 100644
index 0000000..88b489d
--- /dev/null
+++ b/safehttp/plugins/framing/framing_test.go
@@ -0,0 +1,78 @@
+// 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 framing
+
+import (
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing/unsafeframing"
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing/unsafeframingfortests"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+const headerKey = "X-Frame-Options"
+
+func TestFramingProtection(t *testing.T) {
+	req := safehttptest.NewRequest("GET", "https://go.dev/", nil)
+	fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+	xfoInterceptor{}.Before(fakeRW, req, nil)
+	if got, want := rr.Header().Get(headerKey), "SAMEORIGIN"; got != want {
+		t.Errorf(`response.Header().Get(%q): got %q want %q`, headerKey, got, want)
+	}
+}
+
+func TestDisableFramingProtection(t *testing.T) {
+	tests := []struct {
+		name   string
+		config safehttp.InterceptorConfig
+	}{
+		{
+			name:   "unsafeframing",
+			config: unsafeframing.Disable("testing", true),
+		},
+		{
+			name:   "allowlist",
+			config: unsafeframing.Allow("testing", false, "test.goog"),
+		},
+		{
+			name:   "internal",
+			config: internalunsafeframing.Disable{SkipReports: true},
+		},
+		{
+			name:   "testing",
+			config: unsafeframingfortests.Disable(),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := safehttptest.NewRequest("GET", "https://go.dev/", nil)
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+
+			if got, want := (xfoInterceptor{}).Match(tt.config), true; got != want {
+				t.Errorf(`xfoInterceptor{}.Match(%T): got %v, want %v`, tt.config, got, want)
+			}
+
+			xfoInterceptor{}.Before(fakeRW, req, tt.config)
+			if got, want := rr.Header().Get(headerKey), "ALLOWALL"; got != want {
+				t.Errorf(`response.Header().Get(%q): got %q want %q`, headerKey, got, want)
+			}
+		})
+	}
+
+}
diff --git a/safehttp/plugins/framing/internalunsafeframing/unsafeframingfortests/unsafeframingfortests.go b/safehttp/plugins/framing/internalunsafeframing/unsafeframingfortests/unsafeframingfortests.go
new file mode 100644
index 0000000..40d3777
--- /dev/null
+++ b/safehttp/plugins/framing/internalunsafeframing/unsafeframingfortests/unsafeframingfortests.go
@@ -0,0 +1,27 @@
+// 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 unsafeframingfortests can be used to disable Framing protections on specific handler registration in tests.
+//
+// This package should only be used in tests.
+package unsafeframingfortests
+
+import (
+	"github.com/google/go-safeweb/safehttp/plugins/framing/internalunsafeframing"
+)
+
+// Disable turns off framing protection.
+func Disable() internalunsafeframing.Disable {
+	return internalunsafeframing.Disable{}
+}
diff --git a/tests/integration/csp/csp_test.go b/tests/integration/csp/csp_test.go
index 7d43d97..752ba8d 100644
--- a/tests/integration/csp/csp_test.go
+++ b/tests/integration/csp/csp_test.go
@@ -26,8 +26,9 @@ import (
 
 func TestServeMuxInstallCSP(t *testing.T) {
 	mb := safehttp.NewServeMuxConfig(nil)
-	it := csp.Default("")
-	mb.Intercept(&it)
+	for _, i := range csp.Default("") {
+		mb.Intercept(i)
+	}
 
 	var nonce string
 	var err error
@@ -69,7 +70,6 @@ func TestServeMuxInstallCSP(t *testing.T) {
 		"Content-Type": {"text/html; charset=utf-8"},
 		"Content-Security-Policy": {
 			"object-src 'none'; script-src 'unsafe-inline' 'nonce-" + nonce + "' 'strict-dynamic' https: http:; base-uri 'none'",
-			"frame-ancestors 'self';",
 			"require-trusted-types-for 'script'"},
 	}
 	if diff := cmp.Diff(wantHeaders, map[string][]string(rr.Header())); diff != "" {