◐ Off-By-One · answer catalog

go-middleware-client-header-trust

2 answer(s)godockergodocker

go-middleware-client-header-trust

📦 Source in repository (JSON)

Answer 1

Root cause. The vulnerable middleware built Entitlements directly from client-controlled request headers (X-ASCE-Tier, X-ASCE-Quota-Limit, X-ASCE-Features). Any caller could forge X-ASCE-Tier: enterprise and bypass entitlement checks — a classic entitlement-bypass landmine.

The fix (repo: ~/entitlements, module entitlements): 1. Delete every r.Header.Get("X-ASCE-*") read. 2. Derive identity server-side from auth.KeyRecordContextKey (populated only by the authn middleware from verified key lookups — never from headers). 3. Derive tier server-side via tiers.Service.GetAccountTier(apiKeyID), falling back to tiers.Free() when the key is unassigned ((*Tier, nil)) or the db is nil ((nil, error)). 4. Static regression tripwire test forbids any Header.Get("X-ASCE / Header["X-ASCE from reappearing.

Before (vulnerable, deleted):

func EntitlementsFromHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        limit, _ := strconv.ParseInt(r.Header.Get("X-ASCE-Quota-Limit"), 10, 64)
        e := Entitlements{
            TierName:   r.Header.Get("X-ASCE-Tier"),      // ← client-controlled!
            QuotaLimit: limit,                            // ← client-controlled!
            Features:   strings.Split(r.Header.Get("X-ASCE-Features"), ","),
        }
        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), entitlementsCtxKey, e)))
    })
}

After (fixed — middleware/entitlements.go):

func EntitlementsFromKeyRecord(svc *tiers.Service) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            e := resolve(r.Context(), svc) // r's headers are never consulted
            ctx := context.WithValue(r.Context(), entitlementsCtxKey, e)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

func resolve(ctx context.Context, svc *tiers.Service) Entitlements {
    rec, ok := auth.KeyRecordFromContext(ctx) // identity: server-side only
    if !ok {
        return fromTier(tiers.Free()) // unauthenticated caller
    }
    t, err := svc.GetAccountTier(rec.APIKeyID) // tier: server-side only
    if err != nil || t == nil {
        return fromTier(tiers.Free()) // unassigned or DB nil → free
    }
    return fromTier(*t)
}

Supporting pieces: auth.KeyRecordContextKey (typed context key for the verified KeyRecord), and tiers.Service.GetAccountTier with documented (*Tier, nil) = unassigned / (nil, error) = db-nil semantics plus a nil-receiver guard. The old middleware is retained only as entitlements_legacy.go behind //go:build legacy for diff reference — excluded from default builds.

Evidence & signatures

Verified with Go 1.26.0: `go vet ./...` clean, `go build ./...` clean, `go test -race -count=1 ./...` all pass. Grep confirms zero `X-ASCE` reads in production code (only the tripwire test and the build-tagged legacy reference match).

Edge cases tested (`middleware/entitlements_test.go`):
- **Upgrade attempt**: free key + forged `enterprise/999999999` headers → **free**; unauthenticated caller + forged headers → **free**.
- **Downgrade attempt**: premium key + forged `free/1` headers → **premium** (headers have zero effect in *both* directions).
- **Unassigned key** (`GetAccountTier` → `nil, nil`) + forged headers → **free**.
- **DB nil** (`SetDown(true)`) and **nil `*tiers.Service`** + forged headers → **free**.
- **Handler-level enforcement**: free key forging enterprise headers still gets **402**; premium key claiming free still gets **200**.
- **Regression tripwire** `TestNoClientHeaderReads`: statically scans production sources; fails if any `Header.Get("X-ASCE` / `Header["X-ASCE` reappears.
- **Contrast proof** (`-tags legacy`): `TestLegacyShowsTheVulnerability` confirms the old code *would* grant enterprise to an unauthenticated caller from headers alone — the exact bypass the fix closes.

Test count: **11** (8 entitlement-matrix subtests + 3 top-level tests).
{"model": "deepseek-v4-flash", "problem_class": "go-middleware-client-header-trust", "result": "passed", "tests": 11}

Answer 2

Root cause. The vulnerable middleware built Entitlements directly from client-controlled request headers (X-ASCE-Tier, X-ASCE-Quota-Limit, X-ASCE-Features). Any caller could forge X-ASCE-Tier: enterprise and bypass entitlement checks — a classic entitlement-bypass landmine.

The fix (repo: ~/entitlements, module entitlements): 1. Delete every r.Header.Get("X-ASCE-*") read. 2. Derive identity server-side from auth.KeyRecordContextKey (populated only by the authn middleware from verified key lookups — never from headers). 3. Derive tier server-side via tiers.Service.GetAccountTier(apiKeyID), falling back to tiers.Free() when the key is unassigned ((*Tier, nil)) or the db is nil ((nil, error)). 4. Static regression tripwire test forbids any Header.Get("X-ASCE / Header["X-ASCE from reappearing.

Before (vulnerable, deleted):

func EntitlementsFromHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        limit, _ := strconv.ParseInt(r.Header.Get("X-ASCE-Quota-Limit"), 10, 64)
        e := Entitlements{
            TierName:   r.Header.Get("X-ASCE-Tier"),      // ← client-controlled!
            QuotaLimit: limit,                            // ← client-controlled!
            Features:   strings.Split(r.Header.Get("X-ASCE-Features"), ","),
        }
        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), entitlementsCtxKey, e)))
    })
}

After (fixed — middleware/entitlements.go):

func EntitlementsFromKeyRecord(svc *tiers.Service) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            e := resolve(r.Context(), svc) // r's headers are never consulted
            ctx := context.WithValue(r.Context(), entitlementsCtxKey, e)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

func resolve(ctx context.Context, svc *tiers.Service) Entitlements {
    rec, ok := auth.KeyRecordFromContext(ctx) // identity: server-side only
    if !ok {
        return fromTier(tiers.Free()) // unauthenticated caller
    }
    t, err := svc.GetAccountTier(rec.APIKeyID) // tier: server-side only
    if err != nil || t == nil {
        return fromTier(tiers.Free()) // unassigned or DB nil → free
    }
    return fromTier(*t)
}

Supporting pieces: auth.KeyRecordContextKey (typed context key for the verified KeyRecord), and tiers.Service.GetAccountTier with documented (*Tier, nil) = unassigned / (nil, error) = db-nil semantics plus a nil-receiver guard. The old middleware is retained only as entitlements_legacy.go behind //go:build legacy for diff reference — excluded from default builds.

Evidence & signatures

Verified with Go 1.26.0: `go vet ./...` clean, `go build ./...` clean, `go test -race -count=1 ./...` all pass. Grep confirms zero `X-ASCE` reads in production code (only the tripwire test and the build-tagged legacy reference match).

Edge cases tested (`middleware/entitlements_test.go`):
- **Upgrade attempt**: free key + forged `enterprise/999999999` headers → **free**; unauthenticated caller + forged headers → **free**.
- **Downgrade attempt**: premium key + forged `free/1` headers → **premium** (headers have zero effect in *both* directions).
- **Unassigned key** (`GetAccountTier` → `nil, nil`) + forged headers → **free**.
- **DB nil** (`SetDown(true)`) and **nil `*tiers.Service`** + forged headers → **free**.
- **Handler-level enforcement**: free key forging enterprise headers still gets **402**; premium key claiming free still gets **200**.
- **Regression tripwire** `TestNoClientHeaderReads`: statically scans production sources; fails if any `Header.Get("X-ASCE` / `Header["X-ASCE` reappears.
- **Contrast proof** (`-tags legacy`): `TestLegacyShowsTheVulnerability` confirms the old code *would* grant enterprise to an unauthenticated caller from headers alone — the exact bypass the fix closes.

Test count: **11** (8 entitlement-matrix subtests + 3 top-level tests).
{"model": "deepseek-v4-flash", "problem_class": "go-middleware-client-header-trust", "result": "passed", "tests": 11}
Generated from the verified corpus · MIT licensedBack to the catalog