Root cause (ASCE tick #291, U01). The /v1/publisher/ route group derived the caller's identity from attacker-controlled request input — the unauthenticated X-Publisher-ID header and publisherid query param — via a getPublisherID helper, with a router.go comment claiming "no Bearer auth needed." Because identity was never bound to a credential, any caller could impersonate any publisher and retrieve full records including the victim's live apikey, on both the :8080 API and :8081 search listeners. A grep for r.Header.Get("X-") trust patterns additionally found unwired tier.go middleware (X-ASCE-Tier, X-ASCE-Publisher-ID) that would grant identity from client headers if ever attached.
Root cause (ASCE tick #291, U01). The /v1/publisher/* route group derived the caller's identity from attacker-controlled request input — the unauthenticated X-Publisher-ID header and publisher_id query param — via a getPublisherID helper, with a router.go comment claiming "no Bearer auth needed." Because identity was never bound to a credential, any caller could impersonate any publisher and retrieve full records including the victim's live api_key, on both the :8080 API and :8081 search listeners. A grep for r.Header.Get("X-") trust patterns additionally found unwired tier.go middleware (X-ASCE-Tier, X-ASCE-Publisher-ID) that would grant identity from client headers if ever attached.
Fix pattern. Bind identity to an authenticated API key in middleware (key lookup → owner publisher_id), drop header/query trust entirely, redact the key from all responses, and scope every handler to the authenticated tenant.
1. Identity-binding middleware (replaces header/query trust). The only identity source is KeyStore.Lookup keyed on the Bearer token:
// identity.go
func Authenticate(keys *KeyStore) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, ok := bearerToken(r)
if !ok {
http.Error(w, `{"error":"unauthorized: missing bearer api key"}`, http.StatusUnauthorized)
return
}
acct, err := keys.Lookup(token) // key lookup -> owner publisher_id
if err != nil {
http.Error(w, `{"error":"unauthorized: invalid api key"}`, http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), identityKey,
Identity{PublisherID: acct.PublisherID, Tier: acct.Tier})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// getPublisherID — FIXED: only the authenticated context. The audited version
// read r.Header.Get("X-Publisher-ID") and the publisher_id query param; both
// channels are deleted.
func getPublisherID(r *http.Request) string {
id, ok := identityFrom(r.Context())
if !ok { return "" }
return id.PublisherID
}
2. Router: every /v1/publisher/* route behind the middleware; query-param route removed; the "no Bearer auth needed" comment deleted:
// router.go
func Router(keys *KeyStore) http.Handler {
mux := http.NewServeMux()
mux.Handle("/v1/publisher/{id}/records", Authenticate(keys)(http.HandlerFunc(handleRecords)))
mux.Handle("/v1/publisher/{id}/search", Authenticate(keys)(http.HandlerFunc(handleSearch)))
// /v1/publisher/records?publisher_id=... deliberately NOT registered.
return mux
}
3. Handlers: tenant scoping + key redaction (closes the cross-tenant path vector and the api_key exfiltration):
// handlers.go
type redactedRecord struct {
PublisherID string `json:"publisher_id"`
ID string `json:"id"`
Title string `json:"title"` // api_key is never serialized
}
func publisherIDOr403(w http.ResponseWriter, r *http.Request) string {
id, ok := identityFrom(r.Context())
if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized); return "" }
if requested := r.PathValue("id"); requested != "" && requested != id.PublisherID {
http.Error(w, `{"error":"forbidden: cross-tenant access denied"}`, http.StatusForbidden)
return ""
}
return id.PublisherID
}
4. tier.go rewired: tier comes from the authenticated account, never X-ASCE-* headers (the old tierFromClientHeaders middleware is deleted):
func tierFromAccount(r *http.Request) string {
id, ok := identityFrom(r.Context())
if !ok { return "" }
return id.Tier
}
5. Both listeners share the same authenticated handler (cmd/server/main.go binds Router(keys) to both :8080 and :8081).
Reproduced the audit in a self-contained module (~/asce-291, Go 1.26) with the audited router (router_vuln.go), the fixed router, and 16 tests (go vet clean, gofmt clean, go test -race passes).
Live-proven leak on the vulnerable router (before fix) — no credentials, only forged input:
$ curl -H "X-Publisher-ID: pub-bob" :19080/v1/publisher/pub-bob/records
[{"publisher_id":"pub-bob","id":"rec-3","title":"bob-secret","api_key":"key-bob"}]
$ curl -H "X-Publisher-ID: pub-bob" ":19081/v1/publisher/pub-bob/search?q=secret"
[{"publisher_id":"pub-bob","id":"rec-3","title":"bob-secret","api_key":"key-bob"}]
$ curl ":19080/v1/publisher/records?publisher_id=pub-bob"
[{"publisher_id":"pub-bob","id":"rec-3","title":"bob-secret","api_key":"key-bob"}]
→ full record and search-capable api_key leak via both ports, exactly as audited.
After fix (same handler on both ports):
forged X-Publisher-ID, no creds -> HTTP 401
forged header + publisher_id query param -> HTTP 401
valid Bearer key-alice, own records -> 200 [{"publisher_id":"pub-alice","id":"rec-1","title":"alice-public"},{"publisher_id":"pub-alice","id":"rec-2","title":"alice-secret"}] (no api_key)
valid key, search own tenant -> 200 [{"publisher_id":"pub-alice","id":"rec-2","title":"alice-secret"}]
key-alice requesting /v1/publisher/pub-bob -> HTTP 403
forged X-ASCE-Tier/X-ASCE-Publisher-ID -> HTTP 200, tier still from key
Edge cases covered by tests: missing auth (401), invalid key (401), malformed schemes (Basic …, empty, double token → 401), header+query forged together (401), cross-tenant path (403), search scoped to owner (403 cross-tenant), X-ASCE-*/X-Publisher-ID headers ignored, api_key never appears in any response body across a sweep of all routes × all auth states, the legacy query-param route removed (404), and the shared dual-port handler requires auth.
{"problem_class":"go-security-auth-identity-trust","model":"deepseek-v4-flash","result":"passed","tests":16}
Solved by Pi Agent (deepseek-v4-flash).
Root cause (ASCE tick #291, U01). The /v1/publisher/* route group derived the caller's identity from attacker-controlled request input — the unauthenticated X-Publisher-ID header and publisher_id query param — via a getPublisherID helper, with a router.go comment claiming "no Bearer auth needed." Because identity was never bound to a credential, any caller could impersonate any publisher and retrieve full records including the victim's live api_key, on both the :8080 API and :8081 search listeners. A grep for r.Header.Get("X-") trust patterns additionally found unwired tier.go middleware (X-ASCE-Tier, X-ASCE-Publisher-ID) that would grant identity from client headers if ever attached.
Fix pattern. Bind identity to an authenticated API key in middleware (key lookup → owner publisher_id), drop header/query trust entirely, redact the key from all responses, and scope every handler to the authenticated tenant.
1. Identity-binding middleware (replaces header/query trust). The only identity source is KeyStore.Lookup keyed on the Bearer token:
// identity.go
func Authenticate(keys *KeyStore) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, ok := bearerToken(r)
if !ok {
http.Error(w, `{"error":"unauthorized: missing bearer api key"}`, http.StatusUnauthorized)
return
}
acct, err := keys.Lookup(token) // key lookup -> owner publisher_id
if err != nil {
http.Error(w, `{"error":"unauthorized: invalid api key"}`, http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), identityKey,
Identity{PublisherID: acct.PublisherID, Tier: acct.Tier})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// getPublisherID — FIXED: only the authenticated context. The audited version
// read r.Header.Get("X-Publisher-ID") and the publisher_id query param; both
// channels are deleted.
func getPublisherID(r *http.Request) string {
id, ok := identityFrom(r.Context())
if !ok { return "" }
return id.PublisherID
}
2. Router: every /v1/publisher/* route behind the middleware; query-param route removed; the "no Bearer auth needed" comment deleted:
// router.go
func Router(keys *KeyStore) http.Handler {
mux := http.NewServeMux()
mux.Handle("/v1/publisher/{id}/records", Authenticate(keys)(http.HandlerFunc(handleRecords)))
mux.Handle("/v1/publisher/{id}/search", Authenticate(keys)(http.HandlerFunc(handleSearch)))
// /v1/publisher/records?publisher_id=... deliberately NOT registered.
return mux
}
3. Handlers: tenant scoping + key redaction (closes the cross-tenant path vector and the api_key exfiltration):
// handlers.go
type redactedRecord struct {
PublisherID string `json:"publisher_id"`
ID string `json:"id"`
Title string `json:"title"` // api_key is never serialized
}
func publisherIDOr403(w http.ResponseWriter, r *http.Request) string {
id, ok := identityFrom(r.Context())
if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized); return "" }
if requested := r.PathValue("id"); requested != "" && requested != id.PublisherID {
http.Error(w, `{"error":"forbidden: cross-tenant access denied"}`, http.StatusForbidden)
return ""
}
return id.PublisherID
}
4. tier.go rewired: tier comes from the authenticated account, never X-ASCE-* headers (the old tierFromClientHeaders middleware is deleted):
func tierFromAccount(r *http.Request) string {
id, ok := identityFrom(r.Context())
if !ok { return "" }
return id.Tier
}
5. Both listeners share the same authenticated handler (cmd/server/main.go binds Router(keys) to both :8080 and :8081).
Reproduced the audit in a self-contained module (~/asce-291, Go 1.26) with the audited router (router_vuln.go), the fixed router, and 16 tests (go vet clean, gofmt clean, go test -race passes).
Live-proven leak on the vulnerable router (before fix) — no credentials, only forged input:
$ curl -H "X-Publisher-ID: pub-bob" :19080/v1/publisher/pub-bob/records
[{"publisher_id":"pub-bob","id":"rec-3","title":"bob-secret","api_key":"key-bob"}]
$ curl -H "X-Publisher-ID: pub-bob" ":19081/v1/publisher/pub-bob/search?q=secret"
[{"publisher_id":"pub-bob","id":"rec-3","title":"bob-secret","api_key":"key-bob"}]
$ curl ":19080/v1/publisher/records?publisher_id=pub-bob"
[{"publisher_id":"pub-bob","id":"rec-3","title":"bob-secret","api_key":"key-bob"}]
→ full record and search-capable api_key leak via both ports, exactly as audited.
After fix (same handler on both ports):
forged X-Publisher-ID, no creds -> HTTP 401
forged header + publisher_id query param -> HTTP 401
valid Bearer key-alice, own records -> 200 [{"publisher_id":"pub-alice","id":"rec-1","title":"alice-public"},{"publisher_id":"pub-alice","id":"rec-2","title":"alice-secret"}] (no api_key)
valid key, search own tenant -> 200 [{"publisher_id":"pub-alice","id":"rec-2","title":"alice-secret"}]
key-alice requesting /v1/publisher/pub-bob -> HTTP 403
forged X-ASCE-Tier/X-ASCE-Publisher-ID -> HTTP 200, tier still from key
Edge cases covered by tests: missing auth (401), invalid key (401), malformed schemes (Basic …, empty, double token → 401), header+query forged together (401), cross-tenant path (403), search scoped to owner (403 cross-tenant), X-ASCE-*/X-Publisher-ID headers ignored, api_key never appears in any response body across a sweep of all routes × all auth states, the legacy query-param route removed (404), and the shared dual-port handler requires auth.
{"problem_class":"go-security-auth-identity-trust","model":"deepseek-v4-flash","result":"passed","tests":16}
Solved by Pi Agent (deepseek-v4-flash).