106 lines
4.2 KiB
Go
106 lines
4.2 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/middleware"
|
|
"RenewCMS/api/pkg/apperr"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newCrossOriginProtectedHandler(publicURL string) (http.Handler, *int) {
|
|
reached := 0
|
|
handler := middleware.CrossOriginProtection(publicURL)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
reached++
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
return handler, &reached
|
|
}
|
|
|
|
func sendCrossOriginRequest(handler http.Handler, method string, headers map[string]string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(method, "https://cms.example.com/v1/user/invite", strings.NewReader(`{"email":"attacker@evil.example"}`))
|
|
req.Header.Set("Content-Type", "text/plain")
|
|
req.AddCookie(&http.Cookie{Name: "jwt", Value: "session"})
|
|
for name, value := range headers {
|
|
req.Header.Set(name, value)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func TestCrossOriginProtection(t *testing.T) {
|
|
refused := []struct {
|
|
name string
|
|
method string
|
|
headers map[string]string
|
|
}{
|
|
{"a cross-site form POST", http.MethodPost, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
|
|
{"a same-site but cross-origin POST from a sibling subdomain", http.MethodPost, map[string]string{"Sec-Fetch-Site": "same-site", "Origin": "https://blog.example.com"}},
|
|
{"a cross-site DELETE", http.MethodDelete, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
|
|
{"an old browser's cross-origin POST without Sec-Fetch-Site", http.MethodPost, map[string]string{"Origin": "https://evil.example"}},
|
|
}
|
|
for _, tc := range refused {
|
|
t.Run("Refuses "+tc.name, func(t *testing.T) {
|
|
handler, reached := newCrossOriginProtectedHandler("https://cms.example.com")
|
|
|
|
rec := sendCrossOriginRequest(handler, tc.method, tc.headers)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Errorf("expected 403, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), apperr.CrossOriginErrorMessage) {
|
|
t.Errorf("expected the cross-origin message, got %q", rec.Body.String())
|
|
}
|
|
if *reached != 0 {
|
|
t.Error("the handler must not run")
|
|
}
|
|
})
|
|
}
|
|
|
|
allowed := []struct {
|
|
name string
|
|
method string
|
|
headers map[string]string
|
|
}{
|
|
{"a same-origin POST from the SPA", http.MethodPost, map[string]string{"Sec-Fetch-Site": "same-origin", "Origin": "https://cms.example.com"}},
|
|
{"a request typed in the address bar", http.MethodPost, map[string]string{"Sec-Fetch-Site": "none"}},
|
|
{"a non-browser client with neither header", http.MethodPost, nil},
|
|
{"a cross-site GET", http.MethodGet, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
|
|
{"a cross-site preflight", http.MethodOptions, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"}},
|
|
{"a cross-origin POST carrying an explicit Authorization header", http.MethodPost, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://shop.example", "Authorization": "Bearer rcms_key"}},
|
|
{"an old browser's POST from the public URL behind a proxy that rewrites Host", http.MethodPost, map[string]string{"Origin": "https://cms.example.com", "Host": "127.0.0.1:8080"}},
|
|
}
|
|
for _, tc := range allowed {
|
|
t.Run("Allows "+tc.name, func(t *testing.T) {
|
|
handler, reached := newCrossOriginProtectedHandler("https://cms.example.com")
|
|
req := httptest.NewRequest(tc.method, "https://cms.example.com/v1/user/invite", nil)
|
|
for name, value := range tc.headers {
|
|
if name == "Host" {
|
|
req.Host = value
|
|
continue
|
|
}
|
|
req.Header.Set(name, value)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK || *reached != 1 {
|
|
t.Errorf("expected the handler to run, got %d (reached %d)", rec.Code, *reached)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("An unparsable public URL still protects against cross-site requests", func(t *testing.T) {
|
|
handler, reached := newCrossOriginProtectedHandler("::not a url")
|
|
|
|
rec := sendCrossOriginRequest(handler, http.MethodPost, map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"})
|
|
|
|
if rec.Code != http.StatusForbidden || *reached != 0 {
|
|
t.Errorf("expected 403 without reaching the handler, got %d (reached %d)", rec.Code, *reached)
|
|
}
|
|
})
|
|
}
|