Sanitized snapshot of private source 13a965dd6ea705dd92499f7dbeaa00c25c15247d. Require same-origin evidence for unsafe methods, fail closed on invalid authentication middleware configuration, and bound untrusted request metadata. AI-Assistance: OpenAI Codex assisted implementation, testing, and security review. Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
+13
-4
@@ -76,19 +76,28 @@ func RequireHTTPS(next http.Handler) http.Handler {
|
||||
// SameOrigin accepts browser requests that are demonstrably same-origin. It
|
||||
// rejects contradictory fetch metadata even when Origin is absent.
|
||||
func SameOrigin(request *http.Request, allowedOrigin string) bool {
|
||||
if site := strings.ToLower(strings.TrimSpace(request.Header.Get("Sec-Fetch-Site"))); site != "" && site != "same-origin" && site != "none" {
|
||||
site := strings.ToLower(strings.TrimSpace(request.Header.Get("Sec-Fetch-Site")))
|
||||
if site != "" && site != "same-origin" && site != "none" {
|
||||
return false
|
||||
}
|
||||
origin := strings.TrimSpace(request.Header.Get("Origin"))
|
||||
if origin == "" {
|
||||
return true
|
||||
if site == "same-origin" || site == "none" {
|
||||
return true
|
||||
}
|
||||
switch request.Method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
want, err := url.Parse(allowedOrigin)
|
||||
if err != nil || want.Scheme == "" || want.Host == "" || want.Path != "" {
|
||||
if err != nil || want.Scheme == "" || want.Host == "" || want.Path != "" || want.RawQuery != "" || want.Fragment != "" || want.User != nil {
|
||||
return false
|
||||
}
|
||||
got, err := url.Parse(origin)
|
||||
return err == nil && strings.EqualFold(got.Scheme, want.Scheme) && strings.EqualFold(got.Host, want.Host) && got.Path == "" && got.RawQuery == "" && got.Fragment == ""
|
||||
return err == nil && got.User == nil && strings.EqualFold(got.Scheme, want.Scheme) && strings.EqualFold(got.Host, want.Host) && got.Path == "" && got.RawQuery == "" && got.Fragment == ""
|
||||
}
|
||||
|
||||
// CSRFToken binds a purpose to opaque session secret material.
|
||||
|
||||
@@ -27,6 +27,39 @@ func TestSameOriginRejectsCrossSiteAndContradiction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOriginRequiresEvidenceForUnsafeRequests(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "https://example.test/change", nil)
|
||||
if SameOrigin(request, "https://example.test") {
|
||||
t.Fatal("unsafe request without origin evidence accepted")
|
||||
}
|
||||
request.Header.Set("Sec-Fetch-Site", "same-origin")
|
||||
if !SameOrigin(request, "https://example.test") {
|
||||
t.Fatal("same-origin fetch metadata rejected")
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodGet, "https://example.test/read", nil)
|
||||
if !SameOrigin(request, "https://example.test") {
|
||||
t.Fatal("safe request without browser metadata rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOriginRejectsMalformedConfiguredAndPresentedOrigins(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "https://example.test/change", nil)
|
||||
request.Header.Set("Origin", "https://example.test")
|
||||
for _, allowed := range []string{
|
||||
"https://user@example.test",
|
||||
"https://example.test?scope=wrong",
|
||||
"https://example.test#wrong",
|
||||
} {
|
||||
if SameOrigin(request, allowed) {
|
||||
t.Fatalf("configured origin %q accepted", allowed)
|
||||
}
|
||||
}
|
||||
request.Header.Set("Origin", "https://user@example.test")
|
||||
if SameOrigin(request, "https://example.test") {
|
||||
t.Fatal("origin containing user information accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSRFIsPurposeBound(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef0123456789abcdef")
|
||||
token, err := CSRFToken(secret, "account:update")
|
||||
|
||||
Reference in New Issue
Block a user