Repository: google/go-safeweb (go)

Issue:
Add COOP functionality
Cross-Origin-Opener-Policy is a protection against cross-window side channels. It can assume 3 possible values and has 2 possible keys.

Keys: Cross-Origin-Opener-Policy, Cross-Origin-Opener-Policy-Report-Only
Values: same-origin, same-origin-allow-popups, none

We need a plugin that supports this functionality and installs a default policy of Cross-Origin-Opener-Policy: same-origin-allow-popups.

This is low priority since it is a stretch goal.

Pull request description:
Added COOP implementation.
Fixes #108

Patch:
diff --git a/safehttp/plugins/coop/coop.go b/safehttp/plugins/coop/coop.go
new file mode 100644
index 0000000..6df0d0b
--- /dev/null
+++ b/safehttp/plugins/coop/coop.go
@@ -0,0 +1,105 @@
+// 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 coop provides Cross-Origin-Opener-Policy protection. Specification: https://html.spec.whatwg.org/#cross-origin-opener-policies
+package coop
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+)
+
+// Mode represents a COOP mode.
+type Mode string
+
+const (
+	// SameOrigin is the strictest and safest COOP available: windows can keep a reference of windows they open only if they are same-origin.
+	SameOrigin Mode = "same-origin"
+	// SameOriginAllowPopups relaxes the same-origin COOP: windows on this origin that open other windows are allowed to keep a reference, but the opposite is not valid.
+	SameOriginAllowPopups Mode = "same-origin-allow-popups"
+	// UnsafeNone disables COOP: this is the default value in browsers.
+	UnsafeNone Mode = "unsafe-none"
+)
+
+// Policy represents a Cross-Origin-Opener-Policy value.
+type Policy struct {
+	// Mode is the mode for the policy.
+	Mode Mode
+	// ReportingGroup is an optional reporting group that needs to be defined with the Reporting API.
+	ReportingGroup string
+	// ReportOnly makes the policy report-only if set.
+	ReportOnly bool
+}
+
+// String serializes the policy. The returned value can be used as a header value.
+func (p Policy) String() string {
+	if p.ReportingGroup == "" {
+		return string(p.Mode)
+	}
+	return string(p.Mode) + `; report-to "` + p.ReportingGroup + `"`
+}
+
+// NewInterceptor constructs an interceptor that applies the given policies.
+func NewInterceptor(policies ...Policy) Interceptor {
+	var rep []string
+	var enf []string
+	for _, p := range policies {
+		if p.ReportOnly {
+			rep = append(rep, p.String())
+		} else {
+			enf = append(enf, p.String())
+		}
+	}
+	return Interceptor{rep: rep, enf: enf}
+}
+
+// Default returns a same-origin enforcing interceptor with the given (potentially empty) report group.
+func Default(reportGroup string) Interceptor {
+	return NewInterceptor(Policy{Mode: SameOrigin, ReportingGroup: reportGroup})
+}
+
+// Interceptor is the interceptor for COOP.
+type Interceptor struct {
+	rep []string
+	enf []string
+}
+
+// Before claims and sets the Report-Only and Enforcement headers for COOP.
+func (it Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result {
+	if cfg != nil {
+		// We got an override, run its Before phase instead.
+		return Interceptor(cfg.(Overrider)).Before(w, r, nil)
+	}
+	w.Header().Claim("Cross-Origin-Opener-Policy")(it.enf)
+	w.Header().Claim("Cross-Origin-Opener-Policy-Report-Only")(it.rep)
+	return safehttp.NotWritten()
+}
+
+// Commit does nothing.
+func (it Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg safehttp.InterceptorConfig) safehttp.Result {
+	return safehttp.NotWritten()
+}
+
+// Overrider is a safehttp.InterceptorConfig that allows to override COOP for a specific handler.
+type Overrider Interceptor
+
+// Override creates an Overrider with the given policies.
+func Override(policies ...Policy) Overrider {
+	return Overrider(NewInterceptor(policies...))
+}
+
+// Match recognizes just this package Interceptor.
+func (p Overrider) Match(i safehttp.Interceptor) bool {
+	_, ok := i.(Interceptor)
+	return ok
+}


Test patch:
diff --git a/safehttp/plugins/coop/coop_test.go b/safehttp/plugins/coop/coop_test.go
new file mode 100644
index 0000000..d5a79d6
--- /dev/null
+++ b/safehttp/plugins/coop/coop_test.go
@@ -0,0 +1,125 @@
+// 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 coop
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+func TestBefore(t *testing.T) {
+	type want struct {
+		enf, rep []string
+	}
+	var tests = []struct {
+		name                 string
+		interceptor          Interceptor
+		overrider            Overrider
+		want, wantOverridden want
+	}{
+		{
+			name:           "No policies, override on header",
+			interceptor:    NewInterceptor(),
+			overrider:      Override(Policy{Mode: SameOrigin}),
+			wantOverridden: want{enf: []string{"same-origin"}},
+		},
+		{
+			name:        "Default",
+			interceptor: Default("coop"),
+			want:        want{enf: []string{`same-origin; report-to "coop"`}},
+		},
+		{
+			name: "policies, override disables enf",
+			interceptor: NewInterceptor(Policy{
+				Mode:           SameOriginAllowPopups,
+				ReportingGroup: "coop-ap",
+			}, Policy{
+				Mode:           SameOrigin,
+				ReportingGroup: "coop-so",
+				ReportOnly:     true,
+			},
+			),
+			overrider: Override(Policy{
+				Mode:           SameOrigin,
+				ReportingGroup: "coop-so",
+				ReportOnly:     true,
+			}),
+			want: want{
+				enf: []string{`same-origin-allow-popups; report-to "coop-ap"`},
+				rep: []string{`same-origin; report-to "coop-so"`},
+			},
+			wantOverridden: want{
+				rep: []string{`same-origin; report-to "coop-so"`},
+			},
+		},
+		{
+			name: "multiple RO",
+			interceptor: NewInterceptor(Policy{
+				Mode:           SameOriginAllowPopups,
+				ReportingGroup: "coop-ap",
+			}, Policy{
+				Mode:           SameOrigin,
+				ReportingGroup: "coop-so",
+				ReportOnly:     true,
+			}, Policy{
+				Mode:           UnsafeNone,
+				ReportingGroup: "coop-un",
+				ReportOnly:     true,
+			}),
+			want: want{
+				enf: []string{`same-origin-allow-popups; report-to "coop-ap"`},
+				rep: []string{`same-origin; report-to "coop-so"`, `unsafe-none; report-to "coop-un"`},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			check := func(rr *safehttptest.ResponseRecorder, w want) {
+				t.Helper()
+				h := rr.Header()
+				enf, rep := h.Values("Cross-Origin-Opener-Policy"), h.Values("Cross-Origin-Opener-Policy-Report-Only")
+				if diff := cmp.Diff(w.enf, enf); diff != "" {
+					t.Errorf("Enforced COOP -want +got:\n%s", diff)
+				}
+				if diff := cmp.Diff(w.rep, rep); diff != "" {
+					t.Errorf("Report Only COOP -want +got:\n%s", diff)
+				}
+				if rr.Status() != safehttp.StatusOK {
+					t.Errorf("Status: got %v want: %v", rr.Status(), safehttp.StatusOK)
+				}
+				if rr.Body() != "" {
+					t.Errorf("Got body: %q, didn't want one", rr.Body())
+				}
+			}
+			// Non overridden
+			{
+				rr := safehttptest.NewResponseRecorder()
+				req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+				tt.interceptor.Before(rr.ResponseWriter, req, nil)
+				check(rr, tt.want)
+			}
+			// Overridden
+			{
+				rr := safehttptest.NewResponseRecorder()
+				req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+				tt.interceptor.Before(rr.ResponseWriter, req, tt.overrider)
+				check(rr, tt.wantOverridden)
+			}
+		})
+	}
+}