Repository: google/go-safeweb
Pull request #86 (2020-08-14 16:58:10)

safesql: Add first implementation.
This implementation does not yet provide `FromIdentifierComponents` and `FromQuotedIdentifierComponents` as they are SQL-dialect dependent and there seems to not be a good way to create a generic helper for them.

If the need for them arises I'll try to work some magic to implement something that provides a similar functionality.

empijei: This fails to build because we are using an old version of go.

Should we upgrade?

Patch:
diff --git a/.github/workflows/copyright_linter.sh b/.github/workflows/copyright_linter.sh
new file mode 100755
index 0000000..01c4cc0
--- /dev/null
+++ b/.github/workflows/copyright_linter.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+status=0
+lines=$(cat $1 | wc -l)
+for f in $(find . -name "*.go")
+do
+    diff=$(head $f -n $lines | diff $1 -)
+    if [ ! -z "$diff" ]
+    then
+        echo $f
+        echo "< want, > got"
+        echo "${diff}"
+        echo ""
+        status=1
+    fi
+done
+exit $status
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 3ef3f7d..0fba543 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -77,3 +77,21 @@ jobs:
     - name: Go Vet
       run: |
         go vet ./...
+
+  copyright:
+    name: copyright
+    runs-on: ubuntu-18.04
+    steps:
+
+    - name: Set up Go 1.x
+      uses: actions/setup-go@v2
+      with:
+        go-version: ^1.13
+      id: go
+
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v2
+
+    - name: Copyright linter
+      run: |
+        ./.github/workflows/copyright_linter.sh ./.github/workflows/source-code-copyright-header.txt
diff --git a/safehttp/prototype.go b/.github/workflows/source-code-copyright-header.txt
similarity index 58%
rename from safehttp/prototype.go
rename to .github/workflows/source-code-copyright-header.txt
index f174bc1..b331916 100644
--- a/safehttp/prototype.go
+++ b/.github/workflows/source-code-copyright-header.txt
@@ -11,27 +11,3 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-
-package safehttp
-
-import (
-	"net/http"
-)
-
-// Machinery TODO
-type Machinery struct {
-	h HandleFunc
-	d Dispatcher
-}
-
-// NewMachinery TODO
-func NewMachinery(h HandleFunc, d Dispatcher) *Machinery {
-	return &Machinery{h: h, d: d}
-}
-
-// HandleRequest TODO
-func (m *Machinery) HandleRequest(w http.ResponseWriter, req *http.Request) {
-	rw := NewResponseWriter(m.d, w, nil)
-	ir := NewIncomingRequest(req)
-	m.h(rw, ir)
-}
diff --git a/docs/plugins.md b/docs/plugins.md
index 16a77d1..e4002cd 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -8,3 +8,4 @@ responding to incoming requests before they reach the handler.
 
 - [HSTS](plugins/hsts.md): Automatically redirects HTTP traffic to HTTPS and
 sets the `Strict-Transport-Security` header.
+- [staticheaders](plugins/staticheaders.md): Sets the `X-Content-Type-Options` header and the `X-XSS-Protection` header.
diff --git a/docs/plugins/hsts.md b/docs/plugins/hsts.md
index b1e558f..e2f200d 100644
--- a/docs/plugins/hsts.md
+++ b/docs/plugins/hsts.md
@@ -5,7 +5,7 @@ _Authors: grenfeldt@google.com_
 HSTS<sup>1</sup> (HTTP Strict Transport Security) informs browsers that a
 website should only be accessed using HTTPS and not HTTP. This plugin enforces
 HSTS by redirecting all HTTP traffic to HTTPS and by setting the
-`Strict-Transport-Security` header on all outgoing HTTPS traffic.
+`Strict-Transport-Security` header on all HTTPS responses.
 
 1) HSTS: [RFC](https://tools.ietf.org/html/rfc6797),
 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security),
diff --git a/docs/plugins/staticheaders.md b/docs/plugins/staticheaders.md
new file mode 100644
index 0000000..b8d3074
--- /dev/null
+++ b/docs/plugins/staticheaders.md
@@ -0,0 +1,14 @@
+_Authors: grenfeldt@google.com_
+
+# Static headers Plugin
+
+This plugin sets the `X-Content-Type-Options` header to `nosniff` and the
+`X-XSS-Protection` header to `0` on all responses.
+
+- `X-Content-Type-Options: nosniff` tells browsers to not try to sniff the
+`Content-Type` of responses.
+[MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options).
+- `X-XSS-Protection: 0` tells the browser to disable any built in XSS filters.
+These built in XSS filters are unnecessary when other, stronger, protections are
+available and can introduce cross-site leaks vulnerabilities.
+[MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index ef979bb..91b9008 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -23,9 +23,9 @@ import (
 	"github.com/google/go-safeweb/safehttp"
 )
 
-// Plugin implements automatic HSTS functionality.
+// Interceptor implements automatic HSTS functionality.
 // See https://tools.ietf.org/html/rfc6797 for more info.
-type Plugin struct {
+type Interceptor struct {
 	// MaxAge is the duration that the browser should remember
 	// that a site is only to be accessed using HTTPS. MaxAge
 	// must be positive. It will be rounded to seconds before use.
@@ -52,25 +52,25 @@ type Plugin struct {
 	BehindProxy bool
 }
 
-// NewPlugin creates a new HSTS plugin with safe defaults.
+// Default creates a new HSTS interceptor with safe defaults.
 // These safe defaults are:
 //  - max-age set to two years.
 //  - includeSubDomains is enabled.
 //  - preload is disabled.
-func NewPlugin() Plugin {
-	return Plugin{MaxAge: 63072000 * time.Second} // two years in seconds
+func Default() Interceptor {
+	return Interceptor{MaxAge: 63072000 * time.Second} // two years in seconds
 }
 
 // Before should be executed before the request is sent to the handler.
 // The function redirects HTTP requests to HTTPS. When HTTPS traffic
 // is received the Strict-Transport-Security header is applied to the
 // response.
-func (p Plugin) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
-	if p.MaxAge < 0 {
+func (it Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+	if it.MaxAge < 0 {
 		return w.ServerError(safehttp.StatusInternalServerError)
 	}
 
-	if !p.BehindProxy && r.TLS == nil {
+	if !it.BehindProxy && r.TLS == nil {
 		u, err := url.Parse(r.URL.String())
 		if err != nil {
 			return w.ServerError(safehttp.StatusInternalServerError)
@@ -81,19 +81,17 @@ func (p Plugin) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) s
 
 	var value strings.Builder
 	value.WriteString("max-age=")
-	value.WriteString(strconv.FormatInt(int64(p.MaxAge.Seconds()), 10))
-	if !p.DisableIncludeSubDomains {
+	value.WriteString(strconv.FormatInt(int64(it.MaxAge.Seconds()), 10))
+	if !it.DisableIncludeSubDomains {
 		value.WriteString("; includeSubDomains")
 	}
-	if p.Preload {
+	if it.Preload {
 		value.WriteString("; preload")
 	}
-	h := w.Header()
-	if err := h.Set("Strict-Transport-Security", value.String()); err != nil {
-		return w.ServerError(safehttp.StatusInternalServerError)
-	}
-	if _, err := h.Claim("Strict-Transport-Security"); err != nil {
+	set, err := w.Header().Claim("Strict-Transport-Security")
+	if err != nil {
 		return w.ServerError(safehttp.StatusInternalServerError)
 	}
+	set([]string{value.String()})
 	return safehttp.Result{}
 }
diff --git a/safehttp/plugins/staticheaders/staticheaders.go b/safehttp/plugins/staticheaders/staticheaders.go
new file mode 100644
index 0000000..d9c1423
--- /dev/null
+++ b/safehttp/plugins/staticheaders/staticheaders.go
@@ -0,0 +1,41 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package staticheaders
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+)
+
+// Plugin claims and sets static headers on responses.
+type Plugin struct{}
+
+// Before claims and sets the following headers:
+//  - X-Content-Type-Options: nosniff
+//  - X-XSS-Protection: 0
+func (Plugin) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+	h := w.Header()
+	setXCTO, err := h.Claim("X-Content-Type-Options")
+	if err != nil {
+		return w.ServerError(safehttp.StatusInternalServerError)
+	}
+	setXCTO([]string{"nosniff"})
+
+	setXXP, err := h.Claim("X-XSS-Protection")
+	if err != nil {
+		return w.ServerError(safehttp.StatusInternalServerError)
+	}
+	setXXP([]string{"0"})
+	return safehttp.Result{}
+}
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index 48f42f4..aec52aa 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -23,9 +23,8 @@ type ResponseWriter struct {
 	d  Dispatcher
 	rw http.ResponseWriter
 
-	// Having this field unexported is essential for
-	// security. Otherwise one can easily overwrite
-	// the struct bypassing all our safety guarantees.
+	// Having this field unexported is essential for security. Otherwise one can
+	// easily overwrite the struct bypassing all our safety guarantees.
 	header       Header
 	muxInterceps map[string]Interceptor
 }
diff --git a/safesql/internal/raw/raw.go b/safesql/internal/raw/raw.go
new file mode 100644
index 0000000..5f2c602
--- /dev/null
+++ b/safesql/internal/raw/raw.go
@@ -0,0 +1,26 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package raw is used to provide a bypass mechanism to implement unchecked and legacy conversions packages.
+// This package works as a proxy between safesql and any other "conversions" package.
+//
+// The way it functions is to expect safesql to provide the unexported constructors for TrustedSQLString at init() time.
+// Since this package is in internal/ it can only be imported by a parent package, so it is known at compile time that
+// these constructors are not unsafely passed around.
+package raw
+
+// TrustedSQLString is the constructor for a TrustedSQLString to be used by the unchecked and legacy conversions packages.
+// This variable will be assigned by the safesql package at init time.
+// The reason why this is an empty interface is to avoid cyclic dependency between safeslq and this package.
+var TrustedSQLString interface{}
diff --git a/safesql/legacyconversions/legacyconversions.go b/safesql/legacyconversions/legacyconversions.go
new file mode 100644
index 0000000..47638c3
--- /dev/null
+++ b/safesql/legacyconversions/legacyconversions.go
@@ -0,0 +1,31 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package legacyconversions provides functions to create values of package safesql types from plain strings.
+// Use of these functions could potentially result in instances of safesql types that violate their type contracts, and hence result in security vulnerabilities.
+// This package should only be used to gradually migrate to use the safesql package but every use of it should eventually be removed as it represents a security risk.
+package legacyconversions
+
+import (
+	"github.com/google/go-safeweb/safesql"
+	"github.com/google/go-safeweb/safesql/internal/raw"
+)
+
+var trustedSQLStringCtor = raw.TrustedSQLString.(func(string) safesql.TrustedSQLString)
+
+// RiskilyAssumeTrustedSQLString riskily promotes the given string to a trusted string.
+// Uses of this function should only be used when migrating to use safesql and should eventually be removed.
+func RiskilyAssumeTrustedSQLString(trusted string) safesql.TrustedSQLString {
+	return trustedSQLStringCtor(trusted)
+}
diff --git a/safesql/safesql.go b/safesql/safesql.go
new file mode 100644
index 0000000..04ec714
--- /dev/null
+++ b/safesql/safesql.go
@@ -0,0 +1,95 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package safesql implements a safe version of the standard sql package while trying to keep the API as similar as
+// possible to the original one.
+// The concept of this package is to provide "safe by construction" SQL strings so that code that would accidentally introduce
+// SQL injection vulnerabilities does not compile.
+//
+// Examples
+// Code like the following is trivial to migrate from sql to safesql:
+// 	db.Query("SELECT ...", args...)
+// The only change required would be to promote the string literal to a trusted string:
+// 	db.Query(safesql.New("SELECT ..."), args...)
+// For more complicated cases it might be needed to use the helper functions like Join and Concat.
+// If the queries for the service are stored in a trusted runtime-only source that cannot be controlled by a user
+// the uncheckedconversions package can be used to assert that those strings are under the programmer control.
+// Note that unchecked conversions should be very limited, ideally never used, as they pose a security risk.
+//
+// Note on missing documentation
+//
+// For documentation on methods and types that wrap the standard ones please refer to the stdlib package doc instead, as
+// all the types exported by this package are tiny wrappers around the standard ones and thus follow their behavior.
+// The only relevant difference is that functions accept TrustedSQLString instances instead of plain "strings" and that some
+// dangerous methods have been removed.
+package safesql
+
+import (
+	"strconv"
+	"strings"
+
+	"github.com/google/go-safeweb/safesql/internal/raw"
+)
+
+func init() {
+	raw.TrustedSQLString = func(unsafe string) TrustedSQLString { return TrustedSQLString{unsafe} }
+}
+
+type stringConstant string
+
+// TrustedSQLString is a string representing a SQL query that is known to be safe and not contain potentially malicious inputs.
+type TrustedSQLString struct {
+	s string
+}
+
+// New constructs a TrustedSQLString from a compile-time constant string.
+// Since the stringConstant type is unexported the only way to call this function outside of this package is to pass
+// a string literal or an untyped string const.
+func New(text stringConstant) TrustedSQLString { return TrustedSQLString{string(text)} }
+
+// NewFromUint64 constructs a TrustedSQLString from a uint64.
+func NewFromUint64(i uint64) TrustedSQLString { return TrustedSQLString{strconv.FormatUint(i, 10)} }
+
+// TrustedSQLStringConcat concatenates the given trusted SQL strings into a trusted string.
+//
+// Note: this function should not be abused to create arbitrary queries from user input, it is just
+// intended as a helper to compose queries at runtime to avoid redundant constants.
+func TrustedSQLStringConcat(ss ...TrustedSQLString) TrustedSQLString {
+	return TrustedSQLStringJoin(ss, TrustedSQLString{})
+}
+
+// TrustedSQLStringJoin joins the given trusted SQL with the given separator the same way strings.Join would.
+//
+// Note: this function should not be abused to create arbitrary queries from user input, it is just
+// intended as a helper to compose queries at runtime to avoid redundant constants.
+func TrustedSQLStringJoin(ss []TrustedSQLString, sep TrustedSQLString) TrustedSQLString {
+	accum := make([]string, 0, len(ss))
+	for _, s := range ss {
+		accum = append(accum, s.s)
+	}
+	return TrustedSQLString{strings.Join(accum, sep.s)}
+}
+func (t TrustedSQLString) String() string {
+	return t.s
+}
+
+// TrustedSQLStringSplit functions as strings.Split but for TrustedSQLStrings.
+func TrustedSQLStringSplit(s TrustedSQLString, sep TrustedSQLString) []TrustedSQLString {
+	spl := strings.Split(s.s, sep.s)
+	accum := make([]TrustedSQLString, 0, len(spl))
+	for _, s := range spl {
+		accum = append(accum, TrustedSQLString{s})
+	}
+	return accum
+}
diff --git a/safesql/sqlwrap.go b/safesql/sqlwrap.go
new file mode 100644
index 0000000..43cc387
--- /dev/null
+++ b/safesql/sqlwrap.go
@@ -0,0 +1,159 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safesql
+
+import (
+	"context"
+	"database/sql"
+	"database/sql/driver"
+	"time"
+)
+
+func Drivers() []string { return sql.Drivers() }
+
+func Register(name string, driver driver.Driver) { sql.Register(name, driver) }
+
+type ColumnType = sql.ColumnType
+type DBStats = sql.DBStats
+type IsolationLevel = sql.IsolationLevel
+type NamedArg = sql.NamedArg
+type NullBool = sql.NullBool
+type NullFloat64 = sql.NullFloat64
+type NullInt32 = sql.NullInt32
+type NullInt64 = sql.NullInt64
+type NullString = sql.NullString
+type NullTime = sql.NullTime
+type Out = sql.Out
+type RawBytes = sql.RawBytes
+type Result = sql.Result
+type Row = sql.Row
+type Rows = sql.Rows
+type Scanner = sql.Scanner
+type Stmt = sql.Stmt
+type TxOptions = sql.TxOptions
+
+// Conn behaves as the standard SQL package one, with the exception that it does not implement the `Raw` method for security reasons.
+type Conn struct {
+	c *sql.Conn
+}
+
+/*
+func (c *Conn) Raw(f func(driverConn interface{}) error) (err error) {
+	// Dangerous to expose
+}
+*/
+func (c Conn) BeginTx(ctx context.Context, opts *TxOptions) (Tx, error) {
+	t, err := c.c.BeginTx(ctx, opts)
+	return Tx{t}, err
+}
+func (c Conn) Close() error { return c.c.Close() }
+func (c Conn) ExecContext(ctx context.Context, query TrustedSQLString, args ...interface{}) (Result, error) {
+	return c.c.ExecContext(ctx, query.s, args)
+}
+func (c Conn) PingContext(ctx context.Context) error { return c.c.PingContext(ctx) }
+func (c Conn) PrepareContext(ctx context.Context, query TrustedSQLString) (*Stmt, error) {
+	return c.c.PrepareContext(ctx, query.s)
+}
+func (c Conn) QueryContext(ctx context.Context, query TrustedSQLString, args ...interface{}) (*Rows, error) {
+	return c.c.QueryContext(ctx, query.s, args)
+}
+func (c Conn) QueryRowContext(ctx context.Context, query TrustedSQLString, args ...interface{}) *Row {
+	return c.c.QueryRowContext(ctx, query.s, args)
+}
+
+// DB behaves as the standard SQL package one, with the exception that it does not implement the `Driver` method for security reasons.
+type DB struct {
+	db *sql.DB
+}
+
+func Open(driverName, dataSourceName string) (DB, error) {
+	db, err := sql.Open(driverName, dataSourceName)
+	return DB{db}, err
+}
+func OpenDB(c driver.Connector) DB { return DB{sql.OpenDB(c)} }
+func (db DB) Begin() (Tx, error) {
+	t, err := db.db.Begin()
+	return Tx{t}, err
+}
+func (db DB) BeginTx(ctx context.Context, opts *TxOptions) (Tx, error) {
+	t, err := db.db.BeginTx(ctx, opts)
+	return Tx{t}, err
+}
+func (db DB) Close() error { return db.db.Close() }
+func (db DB) Conn(ctx context.Context) (Conn, error) {
+	c, err := db.db.Conn(ctx)
+	return Conn{c}, err
+}
+func (db DB) Exec(query TrustedSQLString, args ...interface{}) (Result, error) {
+	return db.db.Exec(query.s, args)
+}
+func (db DB) ExecContext(ctx context.Context, query TrustedSQLString, args ...interface{}) (Result, error) {
+	return db.db.ExecContext(ctx, query.s, args)
+}
+func (db DB) Ping() error                                   { return db.db.Ping() }
+func (db DB) PingContext(ctx context.Context) error         { return db.db.PingContext(ctx) }
+func (db DB) Prepare(query TrustedSQLString) (*Stmt, error) { return db.db.Prepare(query.s) }
+func (db DB) PrepareContext(ctx context.Context, query TrustedSQLString) (*Stmt, error) {
+	return db.db.PrepareContext(ctx, query.s)
+}
+func (db DB) Query(query TrustedSQLString, args ...interface{}) (*Rows, error) {
+	return db.db.Query(query.s, args)
+}
+func (db DB) QueryContext(ctx context.Context, query TrustedSQLString, args ...interface{}) (*Rows, error) {
+	return db.db.QueryContext(ctx, query.s, args)
+}
+func (db DB) QueryRow(query TrustedSQLString, args ...interface{}) *Row {
+	return db.db.QueryRow(query.s, args)
+}
+func (db DB) QueryRowContext(ctx context.Context, query TrustedSQLString, args ...interface{}) *Row {
+	return db.db.QueryRowContext(ctx, query.s, args)
+}
+
+// Go 1.15 func (db DB) SetConnMaxIdleTime(d time.Duration) { db.db.SetConnMaxIdleTime(d) }
+func (db DB) SetConnMaxLifetime(d time.Duration) { db.db.SetConnMaxLifetime(d) }
+func (db DB) SetMaxIdleConns(n int)              { db.db.SetMaxIdleConns(n) }
+func (db DB) SetMaxOpenConns(n int)              { db.db.SetMaxOpenConns(n) }
+func (db DB) Stats() DBStats                     { return db.db.Stats() }
+
+type Tx struct {
+	tx *sql.Tx
+}
+
+func (tx Tx) Commit() error { return tx.tx.Commit() }
+func (tx Tx) Exec(query TrustedSQLString, args ...interface{}) (Result, error) {
+	return tx.tx.Exec(query.s, args)
+}
+func (tx Tx) ExecContext(ctx context.Context, query TrustedSQLString, args ...interface{}) (Result, error) {
+	return tx.tx.ExecContext(ctx, query.s, args)
+}
+func (tx Tx) Prepare(query TrustedSQLString) (*Stmt, error) { return tx.tx.Prepare(query.s) }
+func (tx Tx) PrepareContext(ctx context.Context, query TrustedSQLString) (*Stmt, error) {
+	return tx.tx.PrepareContext(ctx, query.s)
+}
+func (tx Tx) Query(query TrustedSQLString, args ...interface{}) (*Rows, error) {
+	return tx.tx.Query(query.s, args)
+}
+func (tx Tx) QueryContext(ctx context.Context, query TrustedSQLString, args ...interface{}) (*Rows, error) {
+	return tx.tx.QueryContext(ctx, query.s, args)
+}
+func (tx Tx) QueryRow(query TrustedSQLString, args ...interface{}) *Row {
+	return tx.tx.QueryRow(query.s, args)
+}
+func (tx Tx) QueryRowContext(ctx context.Context, query TrustedSQLString, args ...interface{}) *Row {
+	return tx.tx.QueryRowContext(ctx, query.s, args)
+}
+func (tx Tx) Rollback() error                                   { return tx.tx.Rollback() }
+func (tx Tx) Stmt(stmt *Stmt) *Stmt                             { return tx.tx.Stmt(stmt) }
+func (tx Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt { return tx.tx.StmtContext(ctx, stmt) }
diff --git a/safesql/uncheckedconversions/uncheckedconversions.go b/safesql/uncheckedconversions/uncheckedconversions.go
new file mode 100644
index 0000000..c6b9ed4
--- /dev/null
+++ b/safesql/uncheckedconversions/uncheckedconversions.go
@@ -0,0 +1,33 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package uncheckedconversions provides functions to create values of package safesql types from plain strings.
+// Uses of these functions could potentially result in instances of safesql types that violate their type contracts, and hence result in security vulnerabilities.
+package uncheckedconversions
+
+import (
+	"github.com/google/go-safeweb/safesql"
+	"github.com/google/go-safeweb/safesql/internal/raw"
+)
+
+var trustedSQLStringCtor = raw.TrustedSQLString.(func(string) safesql.TrustedSQLString)
+
+// TrustedSQLStringFromStringKnownToSatisfyTypeContract promotes the given string to a trusted string.
+// Only strings known to be under the programmer control and trusted strings should be passed to this function.
+//
+// One example of correct use of this function would be to cast a query that was retrieved from a query storage to be used with the safesql package.
+// If the query storage is under the programmer control and user input cannot be put into it then the string is known to satisfy the type contract.
+func TrustedSQLStringFromStringKnownToSatisfyTypeContract(trusted string) safesql.TrustedSQLString {
+	return trustedSQLStringCtor(trusted)
+}


Test patch:
diff --git a/safehttp/form_test.go b/safehttp/form_test.go
index a8c546d..003328f 100644
--- a/safehttp/form_test.go
+++ b/safehttp/form_test.go
@@ -37,7 +37,7 @@ func TestFormValidInt(t *testing.T) {
 		{
 			name: "Valid int in POST non-multipart request",
 			req: func() *http.Request {
-				postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza="+stringMaxInt))
+				postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza="+stringMaxInt))
 				postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return postReq
 			}(),
@@ -51,7 +51,7 @@ func TestFormValidInt(t *testing.T) {
 					"\r\n" +
 					stringMaxInt + "\r\n" +
 					"--123--\r\n"
-				multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+				multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 				multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return multipartReq
 			}(),
@@ -60,7 +60,8 @@ func TestFormValidInt(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 				var err error
@@ -84,10 +85,10 @@ func TestFormValidInt(t *testing.T) {
 				t.Errorf("form.Int64:  mismatch (-want +got): \n%s", diff)
 			}
 			return safehttp.Result{}
-		}, &testDispatcher{})
+		}))
 
 		rec := newResponseRecorder(&strings.Builder{})
-		m.HandleRequest(rec, test.req)
+		mux.ServeHTTP(rec, test.req)
 
 		if want := 200; rec.status != want {
 			t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -105,7 +106,7 @@ func TestFormInvalidInt(t *testing.T) {
 			name: "Overflow integer in request",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=9223372036854775810"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=9223372036854775810"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -115,7 +116,7 @@ func TestFormInvalidInt(t *testing.T) {
 						"\r\n" +
 						"9223372036854775810\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -126,7 +127,7 @@ func TestFormInvalidInt(t *testing.T) {
 			name: "Not an integer in request",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=diavola"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=diavola"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 
@@ -137,7 +138,7 @@ func TestFormInvalidInt(t *testing.T) {
 						"\r\n" +
 						"diavola\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -148,7 +149,8 @@ func TestFormInvalidInt(t *testing.T) {
 
 	for _, test := range tests {
 		for _, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -173,10 +175,10 @@ func TestFormInvalidInt(t *testing.T) {
 				}
 
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -195,7 +197,7 @@ func TestFormValidUint(t *testing.T) {
 		{
 			name: "Valid unsigned integer in POST non-multipart request",
 			req: func() *http.Request {
-				postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza="+stringMaxUint))
+				postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza="+stringMaxUint))
 				postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return postReq
 			}(),
@@ -209,7 +211,7 @@ func TestFormValidUint(t *testing.T) {
 					"\r\n" +
 					stringMaxUint + "\r\n" +
 					"--123--\r\n"
-				multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+				multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 				multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return multipartReq
 			}(),
@@ -218,7 +220,8 @@ func TestFormValidUint(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 				var err error
@@ -242,10 +245,10 @@ func TestFormValidUint(t *testing.T) {
 				t.Errorf("form.Uint64:  mismatch (-want +got): \n%s", diff)
 			}
 			return safehttp.Result{}
-		}, &testDispatcher{})
+		}))
 
 		rec := newResponseRecorder(&strings.Builder{})
-		m.HandleRequest(rec, test.req)
+		mux.ServeHTTP(rec, test.req)
 
 		if want := 200; rec.status != want {
 			t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -263,7 +266,7 @@ func TestFormInvalidUint(t *testing.T) {
 			name: "Not an unsigned integer",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=-1"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=-1"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -273,7 +276,7 @@ func TestFormInvalidUint(t *testing.T) {
 						"\r\n" +
 						"-1\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -284,7 +287,7 @@ func TestFormInvalidUint(t *testing.T) {
 			name: "Overflow unsigned integer",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=18446744073709551630"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=18446744073709551630"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -294,7 +297,7 @@ func TestFormInvalidUint(t *testing.T) {
 						"\r\n" +
 						"18446744073709551630\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -304,7 +307,8 @@ func TestFormInvalidUint(t *testing.T) {
 	}
 	for _, test := range tests {
 		for _, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -328,10 +332,10 @@ func TestFormInvalidUint(t *testing.T) {
 					t.Errorf("form.Err: got nil, want error")
 				}
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -350,9 +354,9 @@ func TestFormValidString(t *testing.T) {
 			name: "Valid string in POST non-multipart request",
 			reqs: func() []*http.Request {
 				reqs := []*http.Request{
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=diavola")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=ăȚâȘî")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=\x64\x69\x61\x76\x6f\x6c\x61")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=diavola")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=ăȚâȘî")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=\x64\x69\x61\x76\x6f\x6c\x61")),
 				}
 				for _, req := range reqs {
 					req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -372,7 +376,7 @@ func TestFormValidString(t *testing.T) {
 						"\r\n" +
 						val + "\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					reqs = append(reqs, multipartReq)
 
@@ -385,7 +389,8 @@ func TestFormValidString(t *testing.T) {
 
 	for _, test := range tests {
 		for idx, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -409,10 +414,10 @@ func TestFormValidString(t *testing.T) {
 					t.Errorf("form.Err: got %v, want nil", err)
 				}
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -433,8 +438,8 @@ func TestFormValidFloat64(t *testing.T) {
 			name: "Valid floats in POST non-multipart request",
 			reqs: func() []*http.Request {
 				reqs := []*http.Request{
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza="+stringMaxFloat)),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza="+stringNegativeFloat)),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza="+stringMaxFloat)),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza="+stringNegativeFloat)),
 				}
 				for _, req := range reqs {
 					req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -454,7 +459,7 @@ func TestFormValidFloat64(t *testing.T) {
 						"\r\n" +
 						val + "\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					reqs = append(reqs, multipartReq)
 
@@ -467,7 +472,8 @@ func TestFormValidFloat64(t *testing.T) {
 
 	for _, test := range tests {
 		for idx, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -491,10 +497,10 @@ func TestFormValidFloat64(t *testing.T) {
 					t.Errorf("form.Err: got %v, want nil", err)
 				}
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -513,7 +519,7 @@ func TestFormInvalidFloat64(t *testing.T) {
 			name: "Not a float64 in request",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=diavola"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=diavola"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -523,7 +529,7 @@ func TestFormInvalidFloat64(t *testing.T) {
 						"\r\n" +
 						"diavola\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -534,7 +540,7 @@ func TestFormInvalidFloat64(t *testing.T) {
 			name: "Overflow float64 in request",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=1.797693134862315708145274237317043567981e309"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=1.797693134862315708145274237317043567981e309"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -544,7 +550,7 @@ func TestFormInvalidFloat64(t *testing.T) {
 						"\r\n" +
 						"1.797693134862315708145274237317043567981e309\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -554,7 +560,8 @@ func TestFormInvalidFloat64(t *testing.T) {
 	}
 	for _, test := range tests {
 		for _, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -579,10 +586,10 @@ func TestFormInvalidFloat64(t *testing.T) {
 				}
 
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -601,18 +608,18 @@ func TestFormValidBool(t *testing.T) {
 			name: "Valid booleans in POST non-multipart request",
 			reqs: func() []*http.Request {
 				reqs := []*http.Request{
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=true")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=false")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=True")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=False")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=TRUE")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=FALSE")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=t")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=f")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=T")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=F")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=1")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=0")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=true")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=false")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=True")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=False")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=TRUE")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=FALSE")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=t")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=f")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=T")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=F")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=1")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=0")),
 				}
 				for _, req := range reqs {
 					req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -632,7 +639,7 @@ func TestFormValidBool(t *testing.T) {
 						"\r\n" +
 						val + "\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					reqs = append(reqs, multipartReq)
 
@@ -645,7 +652,8 @@ func TestFormValidBool(t *testing.T) {
 
 	for _, test := range tests {
 		for idx, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -670,10 +678,10 @@ func TestFormValidBool(t *testing.T) {
 				}
 
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -692,8 +700,8 @@ func TestFormInvalidBool(t *testing.T) {
 			name: "Invalid booleans in POST non-multipart request",
 			reqs: func() []*http.Request {
 				reqs := []*http.Request{
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=TruE")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=potato")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=TruE")),
+					httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=potato")),
 				}
 				for _, req := range reqs {
 					req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -713,7 +721,7 @@ func TestFormInvalidBool(t *testing.T) {
 						"\r\n" +
 						val + "\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					reqs = append(reqs, multipartReq)
 
@@ -726,7 +734,8 @@ func TestFormInvalidBool(t *testing.T) {
 
 	for _, test := range tests {
 		for _, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -750,10 +759,10 @@ func TestFormInvalidBool(t *testing.T) {
 					t.Errorf("form.Err: got nil, want error")
 				}
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -775,7 +784,7 @@ func TestFormInvalidSlice(t *testing.T) {
 			name: "Request with multiple types in slice",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=true&pizza=1.3"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=true&pizza=1.3"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -789,7 +798,7 @@ func TestFormInvalidSlice(t *testing.T) {
 						"\r\n" +
 						"1.3\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -799,7 +808,7 @@ func TestFormInvalidSlice(t *testing.T) {
 			name: "Unsupported slice type",
 			reqs: []*http.Request{
 				func() *http.Request {
-					postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizza=true&pizza=1.3"))
+					postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=true&pizza=1.3"))
 					postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 					return postReq
 				}(),
@@ -813,7 +822,7 @@ func TestFormInvalidSlice(t *testing.T) {
 						"\r\n" +
 						"1.3\r\n" +
 						"--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+					multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 					return multipartReq
 				}(),
@@ -823,7 +832,8 @@ func TestFormInvalidSlice(t *testing.T) {
 
 	for _, test := range tests {
 		for _, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+			mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 				var form *safehttp.Form
 				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 					var err error
@@ -848,10 +858,10 @@ func TestFormInvalidSlice(t *testing.T) {
 				}
 
 				return safehttp.Result{}
-			}, &testDispatcher{})
+			}))
 
 			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
+			mux.ServeHTTP(rec, req)
 
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -868,7 +878,7 @@ func TestFormErrorHandling(t *testing.T) {
 		{
 			name: "Post form errors",
 			req: func() *http.Request {
-				postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizzaInt=diavola&pizzaBool=true&pizzaUint=-13"))
+				postReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizzaInt=diavola&pizzaBool=true&pizzaUint=-13"))
 				postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return postReq
 			}(),
@@ -889,7 +899,7 @@ func TestFormErrorHandling(t *testing.T) {
 					"\r\n" +
 					"-13\r\n" +
 					"--123--\r\n"
-				multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
+				multipartReq := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(multipartReqBody))
 				multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return multipartReq
 			}(),
@@ -897,7 +907,8 @@ func TestFormErrorHandling(t *testing.T) {
 	}
 
 	for _, tc := range tests {
-		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
 				var err error
@@ -940,10 +951,10 @@ func TestFormErrorHandling(t *testing.T) {
 			}
 
 			return safehttp.Result{}
-		}, &testDispatcher{})
+		}))
 
 		rec := newResponseRecorder(&strings.Builder{})
-		m.HandleRequest(rec, tc.req)
+		mux.ServeHTTP(rec, tc.req)
 
 		if want := 200; rec.status != want {
 			t.Errorf("response status: got %v, want %v", rec.status, want)
@@ -959,7 +970,7 @@ func TestFormValidSlicePost(t *testing.T) {
 	}{
 		{
 			req: func() *http.Request {
-				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=-8&pizza=9&pizza=-100"))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=-8&pizza=9&pizza=-100"))
 				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return req
 			}(),
@@ -968,7 +979,7 @@ func TestFormValidSlicePost(t *testing.T) {
 		},
 		{
 			req: func() *http.Request {
-				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=8&pizza=9&pizza=10"))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=8&pizza=9&pizza=10"))
 				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return req
 			}(),
@@ -977,7 +988,7 @@ func TestFormValidSlicePost(t *testing.T) {
 		},
 		{
 			req: func() *http.Request {
-				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=margeritta&pizza=diavola&pizza=calzone"))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=margeritta&pizza=diavola&pizza=calzone"))
 				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return req
 			}(),
@@ -986,7 +997,7 @@ func TestFormValidSlicePost(t *testing.T) {
 		},
 		{
 			req: func() *http.Request {
-				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=1.3&pizza=8.9&pizza=-4.1"))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=1.3&pizza=8.9&pizza=-4.1"))
 				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return req
 			}(),
@@ -995,7 +1006,7 @@ func TestFormValidSlicePost(t *testing.T) {
 		},
 		{
 			req: func() *http.Request {
-				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=true&pizza=false&pizza=T"))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader("pizza=true&pizza=false&pizza=T"))
 				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return req
 			}(),
@@ -1005,7 +1016,8 @@ func TestFormValidSlicePost(t *testing.T) {
 	}
 
 	for _, tc := range tests {
-		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			form, err := ir.PostForm()
 			if err != nil {
 				t.Fatalf(`ir.PostForm: got err %v, want nil`, err)
@@ -1021,10 +1033,10 @@ func TestFormValidSlicePost(t *testing.T) {
 			}
 
 			return safehttp.Result{}
-		}, &testDispatcher{})
+		}))
 
 		rec := newResponseRecorder(&strings.Builder{})
-		m.HandleRequest(rec, tc.req)
+		mux.ServeHTTP(rec, tc.req)
 
 		if respStatus, want := rec.status, 200; respStatus != want {
 			t.Errorf("response status: got %v, want %v", respStatus, want)
@@ -1052,7 +1064,7 @@ func TestFormValidSliceMultipart(t *testing.T) {
 			req: func() *http.Request {
 				b := &strings.Builder{}
 				multipartBody.Execute(b, []string{"-8", "9", "-100"})
-				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(b.String()))
 				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return req
 			}(),
@@ -1063,7 +1075,7 @@ func TestFormValidSliceMultipart(t *testing.T) {
 			req: func() *http.Request {
 				b := &strings.Builder{}
 				multipartBody.Execute(b, []string{"8", "9", "10"})
-				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(b.String()))
 				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return req
 			}(),
@@ -1074,7 +1086,7 @@ func TestFormValidSliceMultipart(t *testing.T) {
 			req: func() *http.Request {
 				b := &strings.Builder{}
 				multipartBody.Execute(b, []string{"margeritta", "diavola", "calzone"})
-				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(b.String()))
 				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return req
 			}(),
@@ -1085,7 +1097,7 @@ func TestFormValidSliceMultipart(t *testing.T) {
 			req: func() *http.Request {
 				b := &strings.Builder{}
 				multipartBody.Execute(b, []string{"1.3", "8.9", "-4.1"})
-				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(b.String()))
 				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return req
 			}(),
@@ -1096,7 +1108,7 @@ func TestFormValidSliceMultipart(t *testing.T) {
 			req: func() *http.Request {
 				b := &strings.Builder{}
 				multipartBody.Execute(b, []string{"true", "false", "true"})
-				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req := httptest.NewRequest("POST", "http://foo.com/", strings.NewReader(b.String()))
 				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
 				return req
 			}(),
@@ -1106,7 +1118,8 @@ func TestFormValidSliceMultipart(t *testing.T) {
 	}
 
 	for _, tc := range tests {
-		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+		mux.Handle("/", safehttp.MethodPost, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			form, err := ir.MultipartForm(32 << 20)
 			if err != nil {
 				t.Fatalf(`ir.MultipartForm: got err %v, want nil`, err)
@@ -1121,10 +1134,10 @@ func TestFormValidSliceMultipart(t *testing.T) {
 				t.Errorf("form.Slice: diff (-want +got): \n%v", diff)
 			}
 			return safehttp.Result{}
-		}, &testDispatcher{})
+		}))
 
 		rec := newResponseRecorder(&strings.Builder{})
-		m.HandleRequest(rec, tc.req)
+		mux.ServeHTTP(rec, tc.req)
 
 		if respStatus, want := rec.status, 200; respStatus != want {
 			t.Errorf("response status: got %v, want %v", respStatus, want)
diff --git a/safehttp/plugins/hsts/hsts_test.go b/safehttp/plugins/hsts/hsts_test.go
index 0c62afd..f120acc 100644
--- a/safehttp/plugins/hsts/hsts_test.go
+++ b/safehttp/plugins/hsts/hsts_test.go
@@ -27,27 +27,27 @@ import (
 func TestHSTS(t *testing.T) {
 	var test = []struct {
 		name        string
-		plugin      hsts.Plugin
+		interceptor hsts.Interceptor
 		req         *safehttp.IncomingRequest
 		wantStatus  int
 		wantHeaders map[string][]string
 		wantBody    string
 	}{
 		{
-			name:       "HTTPS",
-			plugin:     hsts.NewPlugin(),
-			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
-			wantStatus: 200,
+			name:        "HTTPS",
+			interceptor: hsts.Default(),
+			req:         safehttptest.NewRequest("GET", "https://localhost/", nil),
+			wantStatus:  200,
 			wantHeaders: map[string][]string{
 				"Strict-Transport-Security": {"max-age=63072000; includeSubDomains"}, // 63072000 seconds is two years
 			},
 			wantBody: "",
 		},
 		{
-			name:       "HTTP",
-			plugin:     hsts.NewPlugin(),
-			req:        safehttptest.NewRequest("GET", "http://localhost/", nil),
-			wantStatus: 301,
+			name:        "HTTP",
+			interceptor: hsts.Default(),
+			req:         safehttptest.NewRequest("GET", "http://localhost/", nil),
+			wantStatus:  301,
 			wantHeaders: map[string][]string{
 				"Content-Type": {"text/html; charset=utf-8"},
 				"Location":     {"https://localhost/"},
@@ -55,10 +55,10 @@ func TestHSTS(t *testing.T) {
 			wantBody: "<a href=\"https://localhost/\">Moved Permanently</a>.\n\n",
 		},
 		{
-			name:       "HTTP behind proxy",
-			plugin:     hsts.Plugin{BehindProxy: true},
-			req:        safehttptest.NewRequest("GET", "http://localhost/", nil),
-			wantStatus: 200,
+			name:        "HTTP behind proxy",
+			interceptor: hsts.Interceptor{BehindProxy: true},
+			req:         safehttptest.NewRequest("GET", "http://localhost/", nil),
+			wantStatus:  200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
 				"Strict-Transport-Security": {"max-age=0; includeSubDomains"},
@@ -66,10 +66,10 @@ func TestHSTS(t *testing.T) {
 			wantBody: "",
 		},
 		{
-			name:       "Preload",
-			plugin:     hsts.Plugin{Preload: true, DisableIncludeSubDomains: true},
-			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
-			wantStatus: 200,
+			name:        "Preload",
+			interceptor: hsts.Interceptor{Preload: true, DisableIncludeSubDomains: true},
+			req:         safehttptest.NewRequest("GET", "https://localhost/", nil),
+			wantStatus:  200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
 				"Strict-Transport-Security": {"max-age=0; preload"},
@@ -77,10 +77,10 @@ func TestHSTS(t *testing.T) {
 			wantBody: "",
 		},
 		{
-			name:       "Preload and IncludeSubDomains",
-			plugin:     hsts.Plugin{Preload: true},
-			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
-			wantStatus: 200,
+			name:        "Preload and IncludeSubDomains",
+			interceptor: hsts.Interceptor{Preload: true},
+			req:         safehttptest.NewRequest("GET", "https://localhost/", nil),
+			wantStatus:  200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
 				"Strict-Transport-Security": {"max-age=0; includeSubDomains; preload"},
@@ -88,10 +88,10 @@ func TestHSTS(t *testing.T) {
 			wantBody: "",
 		},
 		{
-			name:       "No preload and no includeSubDomains",
-			plugin:     hsts.Plugin{DisableIncludeSubDomains: true},
-			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
-			wantStatus: 200,
+			name:        "No preload and no includeSubDomains",
+			interceptor: hsts.Interceptor{DisableIncludeSubDomains: true},
+			req:         safehttptest.NewRequest("GET", "https://localhost/", nil),
+			wantStatus:  200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
 				"Strict-Transport-Security": {"max-age=0"},
@@ -99,20 +99,20 @@ func TestHSTS(t *testing.T) {
 			wantBody: "",
 		},
 		{
-			name:       "Custom maxage",
-			plugin:     hsts.Plugin{MaxAge: 3600 * time.Second},
-			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
-			wantStatus: 200,
+			name:        "Custom maxage",
+			interceptor: hsts.Interceptor{MaxAge: 3600 * time.Second},
+			req:         safehttptest.NewRequest("GET", "https://localhost/", nil),
+			wantStatus:  200,
 			wantHeaders: map[string][]string{
 				"Strict-Transport-Security": {"max-age=3600; includeSubDomains"}, // 3600 seconds is 1 hour
 			},
 			wantBody: "",
 		},
 		{
-			name:       "Negative maxage",
-			plugin:     hsts.Plugin{MaxAge: -1 * time.Second},
-			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
-			wantStatus: 500,
+			name:        "Negative maxage",
+			interceptor: hsts.Interceptor{MaxAge: -1 * time.Second},
+			req:         safehttptest.NewRequest("GET", "https://localhost/", nil),
+			wantStatus:  500,
 			wantHeaders: map[string][]string{
 				"Content-Type":           {"text/plain; charset=utf-8"},
 				"X-Content-Type-Options": {"nosniff"},
@@ -125,7 +125,7 @@ func TestHSTS(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			rr := safehttptest.NewResponseRecorder()
 
-			tt.plugin.Before(rr.ResponseWriter, tt.req)
+			tt.interceptor.Before(rr.ResponseWriter, tt.req)
 
 			if rr.Status() != tt.wantStatus {
 				t.Errorf("status code got: %v want: %v", rr.Status(), tt.wantStatus)
@@ -143,7 +143,7 @@ func TestHSTS(t *testing.T) {
 }
 
 func TestStrictTransportSecurityAlreadyImmutable(t *testing.T) {
-	p := hsts.NewPlugin()
+	p := hsts.Default()
 
 	req := safehttptest.NewRequest("GET", "https://localhost/", nil)
 
diff --git a/safehttp/plugins/staticheaders/staticheaders_test.go b/safehttp/plugins/staticheaders/staticheaders_test.go
new file mode 100644
index 0000000..c230133
--- /dev/null
+++ b/safehttp/plugins/staticheaders/staticheaders_test.go
@@ -0,0 +1,81 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package staticheaders_test
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/staticheaders"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+func TestPlugin(t *testing.T) {
+	req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+	rr := safehttptest.NewResponseRecorder()
+
+	p := staticheaders.Plugin{}
+	p.Before(rr.ResponseWriter, req)
+
+	if got, want := rr.Status(), int(safehttp.StatusOK); got != want {
+		t.Errorf("rr.Status() got: %v want: %v", got, want)
+	}
+
+	wantHeaders := map[string][]string{
+		"X-Content-Type-Options": {"nosniff"},
+		"X-Xss-Protection":       {"0"},
+	}
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rr.Header())); diff != "" {
+		t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+	}
+
+	if got, want := rr.Body(), ""; got != want {
+		t.Errorf("rr.Body() got: %q want: %q", got, want)
+	}
+}
+
+func TestAlreadyClaimed(t *testing.T) {
+	alreadyClaimed := []string{"X-Content-Type-Options", "X-XSS-Protection"}
+
+	for _, h := range alreadyClaimed {
+		t.Run(h, func(t *testing.T) {
+			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+			rr := safehttptest.NewResponseRecorder()
+			if _, err := rr.ResponseWriter.Header().Claim(h); err != nil {
+				t.Fatalf("rr.ResponseWriter.Header().Claim(h) got: %v want: nil", err)
+			}
+
+			p := staticheaders.Plugin{}
+			p.Before(rr.ResponseWriter, req)
+
+			if got, want := rr.Status(), int(safehttp.StatusInternalServerError); got != want {
+				t.Errorf("rr.Status() got: %v want: %v", got, want)
+			}
+
+			wantHeaders := map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			}
+			if diff := cmp.Diff(wantHeaders, map[string][]string(rr.Header())); diff != "" {
+				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
+			}
+
+			if got, want := rr.Body(), "Internal Server Error\n"; got != want {
+				t.Errorf("rr.Body() got: %q want: %q", got, want)
+			}
+		})
+	}
+}
diff --git a/safesql/safesql_test.go b/safesql/safesql_test.go
new file mode 100644
index 0000000..0b832b7
--- /dev/null
+++ b/safesql/safesql_test.go
@@ -0,0 +1,121 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safesql
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func TestTrustedSQLConcat(t *testing.T) {
+	var tests = []struct {
+		name string
+		ss   []TrustedSQLString
+		want TrustedSQLString
+	}{
+		{name: "nothing"},
+		{
+			name: "one word",
+			ss:   []TrustedSQLString{New("foo")},
+			want: New("foo"),
+		},
+		{
+			name: "two words",
+			ss:   []TrustedSQLString{New("foo"), New("ffa")},
+			want: New("fooffa"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := TrustedSQLStringConcat(tt.ss...)
+			if got != tt.want {
+				t.Errorf("got: %v, want: %v", got, tt.want)
+			}
+		})
+	}
+}
+func TestTrustedSQLJoin(t *testing.T) {
+	var tests = []struct {
+		name string
+		ss   []TrustedSQLString
+		sep  TrustedSQLString
+		want TrustedSQLString
+	}{
+		{name: "nothing"},
+		{
+			name: "one word",
+			ss:   []TrustedSQLString{New("foo")},
+			sep:  New("bar"),
+			want: New("foo"),
+		},
+		{
+			name: "two words",
+			ss:   []TrustedSQLString{New("foo"), New("ffa")},
+			sep:  New("bar"),
+			want: New("foobarffa"),
+		},
+		{
+			name: "empty sep",
+			ss:   []TrustedSQLString{New("foo"), New("ffa")},
+			want: New("fooffa"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := TrustedSQLStringJoin(tt.ss, tt.sep)
+			if got != tt.want {
+				t.Errorf("got: %v, want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestTrustedSQLStringSplit(t *testing.T) {
+	var tests = []struct {
+		name string
+		in   TrustedSQLString
+		sep  TrustedSQLString
+		want []TrustedSQLString
+	}{
+		{name: "nothing"},
+		{
+			name: "no sep",
+			in:   New("foo"),
+			sep:  New("bar"),
+			want: []TrustedSQLString{New("foo")},
+		},
+		{
+			name: "two words",
+			in:   New("foobarffa"),
+			sep:  New("bar"),
+			want: []TrustedSQLString{New("foo"), New("ffa")},
+		},
+		{
+			name: "empty sep",
+			in:   New("foo"),
+			want: []TrustedSQLString{New("f"), New("o"), New("o")},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := TrustedSQLStringSplit(tt.in, tt.sep)
+			if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty(), cmp.AllowUnexported(TrustedSQLString{})); diff != "" {
+				t.Errorf("-want +got: %s", diff)
+			}
+		})
+	}
+}
diff --git a/tests/formparams/form_params_test.go b/tests/formparams/form_params_test.go
index 5d14929..4f4c1c8 100644
--- a/tests/formparams/form_params_test.go
+++ b/tests/formparams/form_params_test.go
@@ -4,7 +4,7 @@
 // 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
+// 	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,
diff --git a/tests/integration/header/header_test.go b/tests/integration/header/header_test.go
index bd4c1e8..969b5c0 100644
--- a/tests/integration/header/header_test.go
+++ b/tests/integration/header/header_test.go
@@ -66,15 +66,16 @@ func (r *responseRecorder) Write(data []byte) (int, error) {
 }
 
 func TestAccessIncomingHeaders(t *testing.T) {
-	m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
-		if got, want := r.Header.Get("A"), "B"; got != want {
-			t.Errorf(`r.Header.Get("A") got: %v want: %v`, got, want)
+	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+		if got, want := ir.Header.Get("A"), "B"; got != want {
+			t.Errorf(`ir.Header.Get("A") got: %v want: %v`, got, want)
 		}
 		return rw.Write(safehtml.HTMLEscaped("hello"))
-	}, &testDispatcher{})
+	}))
 
 	request := "GET / HTTP/1.1\r\n" +
-		"Host: localhost:8080\r\n" +
+		"Host: foo.com\r\n" +
 		"A: B\r\n\r\n"
 	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(request)))
 	if err != nil {
@@ -84,21 +85,22 @@ func TestAccessIncomingHeaders(t *testing.T) {
 	b := &strings.Builder{}
 	rw := newResponseRecorder(b)
 
-	m.HandleRequest(rw, req)
+	mux.ServeHTTP(rw, req)
 }
 
 func TestChangingResponseHeaders(t *testing.T) {
-	m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		rw.Header().Set("pIZZA", "Pasta")
 		return rw.Write(safehtml.HTMLEscaped("hello"))
-	}, &testDispatcher{})
+	}))
 
-	req := httptest.NewRequest("GET", "/", nil)
+	req := httptest.NewRequest("GET", "http://foo.com/", nil)
 
 	b := &strings.Builder{}
 	rw := newResponseRecorder(b)
 
-	m.HandleRequest(rw, req)
+	mux.ServeHTTP(rw, req)
 
 	want := []string{"Pasta"}
 	if diff := cmp.Diff(want, rw.Header()["Pizza"]); diff != "" {
diff --git a/tests/integration/hsts/hsts_test.go b/tests/integration/hsts/hsts_test.go
index 1fa51df..b358f89 100644
--- a/tests/integration/hsts/hsts_test.go
+++ b/tests/integration/hsts/hsts_test.go
@@ -73,7 +73,7 @@ func TestHSTSServeMuxInstall(t *testing.T) {
 	})
 	mux.Handle("/asdf", safehttp.MethodGet, handler)
 
-	mux.Install("hsts", hsts.NewPlugin())
+	mux.Install("hsts", hsts.Default())
 
 	b := strings.Builder{}
 	rr := newResponseRecorder(&b)
diff --git a/tests/integration/safehtml/safehtml_test.go b/tests/integration/safehtml/safehtml_test.go
index 05d9fe6..eb01a50 100644
--- a/tests/integration/safehtml/safehtml_test.go
+++ b/tests/integration/safehtml/safehtml_test.go
@@ -70,16 +70,17 @@ func (r *responseRecorder) Write(data []byte) (int, error) {
 }
 
 func TestHandleRequestWrite(t *testing.T) {
-	m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		return rw.Write(safehtml.HTMLEscaped("<h1>Escaped, so not really a heading</h1>"))
-	}, &testDispatcher{})
+	}))
 
-	req := httptest.NewRequest("GET", "/", nil)
+	req := httptest.NewRequest("GET", "http://foo.com/", nil)
 
 	b := &strings.Builder{}
 	rw := newResponseRecorder(b)
 
-	m.HandleRequest(rw, req)
+	mux.ServeHTTP(rw, req)
 
 	body := b.String()
 
@@ -89,16 +90,17 @@ func TestHandleRequestWrite(t *testing.T) {
 }
 
 func TestHandleRequestWriteTemplate(t *testing.T) {
-	m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+	mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 		return rw.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
-	}, &testDispatcher{})
+	}))
 
-	req := httptest.NewRequest("GET", "/", nil)
+	req := httptest.NewRequest("GET", "http://foo.com/", nil)
 
 	b := &strings.Builder{}
 	rw := newResponseRecorder(b)
 
-	m.HandleRequest(rw, req)
+	mux.ServeHTTP(rw, req)
 
 	body := b.String()
 
diff --git a/tests/integration/staticheaders/staticheaders_test.go b/tests/integration/staticheaders/staticheaders_test.go
new file mode 100644
index 0000000..bb3a697
--- /dev/null
+++ b/tests/integration/staticheaders/staticheaders_test.go
@@ -0,0 +1,106 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package staticheaders_test
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"text/template"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/staticheaders"
+	"github.com/google/safehtml"
+)
+
+type dispatcher struct{}
+
+func (dispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
+	switch x := resp.(type) {
+	case safehtml.HTML:
+		_, err := rw.Write([]byte(x.String()))
+		return err
+	default:
+		panic("not a safe response type")
+	}
+}
+
+func (dispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
+	switch x := t.(type) {
+	case *template.Template:
+		return x.Execute(rw, data)
+	default:
+		panic("not a safe response type")
+	}
+}
+
+type responseRecorder struct {
+	header http.Header
+	writer io.Writer
+	status int
+}
+
+func newResponseRecorder(w io.Writer) *responseRecorder {
+	return &responseRecorder{header: http.Header{}, writer: w, status: http.StatusOK}
+}
+
+func (r *responseRecorder) Header() http.Header {
+	return r.header
+}
+
+func (r *responseRecorder) WriteHeader(statusCode int) {
+	r.status = statusCode
+}
+
+func (r *responseRecorder) Write(data []byte) (int, error) {
+	return r.writer.Write(data)
+}
+
+func TestServeMuxInstallStaticHeaders(t *testing.T) {
+	mux := safehttp.NewServeMux(dispatcher{}, "foo.com")
+
+	handler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+	})
+	mux.Handle("/asdf", safehttp.MethodGet, handler)
+
+	mux.Install("staticheaders", staticheaders.Plugin{})
+
+	b := strings.Builder{}
+	rr := newResponseRecorder(&b)
+
+	req := httptest.NewRequest(http.MethodGet, "https://foo.com/asdf", nil)
+
+	mux.ServeHTTP(rr, req)
+
+	if want := int(safehttp.StatusOK); rr.status != want {
+		t.Errorf("rr.status got: %v want: %v", rr.status, want)
+	}
+
+	wantHeaders := map[string][]string{
+		"X-Content-Type-Options": {"nosniff"},
+		"X-Xss-Protection":       {"0"},
+	}
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rr.header)); diff != "" {
+		t.Errorf("rr.header mismatch (-want +got):\n%s", diff)
+	}
+
+	if got, want := b.String(), "&lt;h1&gt;Hello World!&lt;/h1&gt;"; got != want {
+		t.Errorf("b.String() got: %v want: %v", got, want)
+	}
+}