hub (unreleased, SECURITY): /preferences and /notify refuse a per-customer key acting for another household (403); red-proved
gates / gates (push) Successful in 5m49s
gates / gates (push) Successful in 5m49s
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:
@@ -2516,7 +2516,10 @@ func (h *Handler) handleCustomerHistory(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
// handleNotify processes notification events from customer controllers.
|
||||
func (h *Handler) handleNotify(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.checkAuth(r) {
|
||||
// A per-customer key acts only for its own customer (the body's customer_id is checked below); the global key for
|
||||
// any. Found 2026-10-09: this route checked only that the key was valid (crosscustomer_prefs_test.go).
|
||||
authCustomerID, isGlobal, ok := h.checkAuthCustomer(r)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -2538,6 +2541,10 @@ func (h *Handler) handleNotify(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Invalid payload: customer_id and event_type required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isGlobal && authCustomerID != payload.CustomerID {
|
||||
http.Error(w, "Forbidden: customer_id does not match the key", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Printf("[INFO] Notification from %s: %s (%s) — %s", payload.CustomerID, payload.EventType, payload.Severity, payload.Message)
|
||||
|
||||
@@ -2618,7 +2625,10 @@ func (h *Handler) handleNotify(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleSavePreferences stores notification preferences pushed from a customer controller.
|
||||
func (h *Handler) handleSavePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.checkAuth(r) {
|
||||
// A per-customer key acts only for its own customer (the body's customer_id is checked below); the global key for
|
||||
// any. Found 2026-10-09: this route checked only that the key was valid (crosscustomer_prefs_test.go).
|
||||
authCustomerID, isGlobal, ok := h.checkAuthCustomer(r)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -2643,6 +2653,10 @@ func (h *Handler) handleSavePreferences(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "Invalid payload: customer_id required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isGlobal && authCustomerID != payload.CustomerID {
|
||||
http.Error(w, "Forbidden: customer_id does not match the key", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Empty-email no-clobber guard (v0.71.0, audit F12): a controller push with an empty email
|
||||
// (e.g. an unconfigured box) must never wipe a stored non-empty address — the seeded/edited
|
||||
|
||||
Reference in New Issue
Block a user