hub R-138: refuse a token whose one zone also holds another customer's domain (security review)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-08 14:31:57 +02:00
parent 700a2d06fe
commit 501312a4d2
2 changed files with 45 additions and 5 deletions
+12
View File
@@ -1880,6 +1880,18 @@ func (s *Server) cfTokenReachMessage(ctx context.Context, customerID, domain, to
s.logger.Printf("[WARN] cloudflare token check for %s: the token's one zone %q does not cover domain %q — save refused", customerID, names[0], domain)
return fmt.Sprintf("The Cloudflare API token reaches zone %q, which is not this customer's domain %q. Nothing was saved.", names[0], domain)
}
// A zone ABOVE the customer's domain may also hold another customer's sibling domain (a.parent.hu and
// b.parent.hu under zone parent.hu pass the R-415 guard, which compares the two domains only). So the zone
// itself must not equal, contain or lie under any other customer's domain, nor under felhom.eu. Security
// review 2026-10-08; pinned by TestR138_ParentZoneHoldingAnotherCustomerRefused.
if other, err := s.store.DomainConflict(customerID, names[0]); err != nil || other != "" {
if err != nil {
s.logger.Printf("[ERROR] cloudflare token check for %s: the zone could not be checked against the other customers: %v — save refused", customerID, err)
return "The Cloudflare API token's zone could not be checked against the other customers — nothing was saved. Try again."
}
s.logger.Printf("[WARN] cloudflare token check for %s: the token's zone %q also covers customer %s's domain — save refused (R-138)", customerID, names[0], other)
return fmt.Sprintf("The Cloudflare API token reaches zone %q, which also holds the domain of customer %q — make the customer's own domain a zone of its own. Nothing was saved.", names[0], other)
}
s.logger.Printf("[INFO] cloudflare token check for %s: the token sees 1 zone (%s), which covers the customer's domain — allowed", customerID, names[0])
return ""
}
+33 -5
View File
@@ -21,11 +21,12 @@ import (
// customer „b" with the account-wide token → FAILS.
const (
tokOwn = "tok-own-zone-0123456789abcdef"
tokWide = "tok-account-wide-0123456789abcdef"
tokOther = "tok-other-zone-0123456789abcdef"
tokNone = "tok-sees-nothing-0123456789abcdef"
tokOwn2 = "tok-own-zone-second-0123456789abcdef"
tokOwn = "tok-own-zone-0123456789abcdef"
tokWide = "tok-account-wide-0123456789abcdef"
tokOther = "tok-other-zone-0123456789abcdef"
tokNone = "tok-sees-nothing-0123456789abcdef"
tokOwn2 = "tok-own-zone-second-0123456789abcdef"
tokParent = "tok-parent-zone-0123456789abcdef"
)
type fakeCF struct {
@@ -56,6 +57,8 @@ func newFakeCF(t *testing.T) *fakeCF {
zones = []string{"neighbour.hu"}
case tokNone:
zones = nil
case tokParent:
zones = []string{"parent.hu"}
default:
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"success":false,"errors":[{"message":"Invalid API Token"}]}`))
@@ -233,3 +236,28 @@ func TestR138_TokenNeverInLogsOrPage(t *testing.T) {
}
}
}
// A token whose one zone sits ABOVE the customer's domain and also holds another customer's sibling domain is
// refused (security review 2026-10-08: the R-415 guard compares the two domains only, and a.parent.hu and
// b.parent.hu do not overlap). Control: the same parent-zone token is accepted when no other customer lives under it.
// RED-PROOF: remove the DomainConflict(zone) block in cfTokenReachMessage → customer „a" is stored → FAILS.
func TestR138_ParentZoneHoldingAnotherCustomerRefused(t *testing.T) {
s, _, _ := r138Server(t)
if rr := r138Create(s, "b", "b.parent.hu", ""); rr.Code != http.StatusSeeOther {
t.Fatalf("setup: customer b without a token must be created; got %d %s", rr.Code, rr.Body.String())
}
rr := r138Create(s, "a", "a.parent.hu", tokParent)
if rr.Code == http.StatusSeeOther {
t.Fatalf("a token for zone parent.hu, which also holds customer b, was accepted")
}
if !strings.Contains(rr.Body.String(), "also holds the domain of customer") {
t.Errorf("the refusal must name the shared zone; got %d", rr.Code)
}
if _, ok := r138StoredToken(t, s, "a"); ok {
t.Errorf("a config was stored for a refused parent-zone token")
}
s2, _, _ := r138Server(t)
if rr := r138Create(s2, "a", "a.parent.hu", tokParent); rr.Code != http.StatusSeeOther {
t.Errorf("control: the parent-zone token with no other customer under it must be accepted; got %d %s", rr.Code, rr.Body.String())
}
}