java-topology/whitepaper/outreach/caddy.md

4 KiB
Raw Blame History

Caddy — CWE-407 Disclosure Brief

2026-03-27 · Patch available — awaiting upstream merge

Finding

One algorithmic complexity defect in Caddy's reverse proxy load balancer. The consistent-hashing policy recalculates xxhash for all upstream hosts on every request instead of using a pre-computed hash ring, producing O(N) hash work per request. Patched.

The Defects

caddy-0001 (PATCHED — HIGH): modules/caddyhttp/reverseproxy/

// hostByHashing() — called per request for consistent-hash policy:
func (r *reverseProxy) hostByHashing(req *http.Request, pool UpstreamPool) *Upstream {
    key := r.hashKey(req)
    for _, upstream := range pool {
        h := xxhash.Sum64String(upstream.Dial + key)
        // compare h to select upstream
    }
    // O(N) xxhash recalculation per request — N = number of upstream hosts
}

hostByHashing() recomputes xxhash.Sum64String(upstream.Dial + key) for every upstream on every request. For N upstream hosts and R requests per second: O(R × N) hash operations. The upstream dial addresses are static between config reloads — their base hashes can be pre-computed once and stored in a sorted ring. Fix: pre-computed hash ring — sort upstreams by xxhash(upstream.Dial) at config load, then per-request work reduces to one xxhash(key) + O(log N) binary search for ring position.

Complexity Proof

Let N = number of configured upstream hosts (backends), R = requests per second using the consistent-hash load balancing policy.

  • Defective: xxhash.Sum64String(upstream.Dial + key) called N times per request.
    • upstream.Dial is static between reloads; key is per-request.
    • Total hash operations per second: O(R × N).
  • Fixed: pre-computed ring with sorted xxhash(upstream.Dial) values.
    • Per-request work: one xxhash(key) + binary search over N pre-computed positions.
    • Total per second: O(R × (1 + log N)) hash + compare operations.

At N=50 upstreams: defective=50 hash calls per request, fixed=1 hash + 6 comparisons per request. ~8× reduction in hash work per request; speedup grows linearly with N.

The pre-computed ring also enables standard consistent-hashing jump/virtual-node distributions, improving load balance uniformity beyond the current implementation.

Impact

All Caddy deployments using the consistent_hashing load balancing policy in reverse proxy configurations. Caddy is widely used as a production reverse proxy with automatic HTTPS, particularly in developer-facing and smaller-scale production environments. The hot path fires on every request routed through a consistent-hash upstream group.

The Fix

Pre-compute the hash ring at config load in Provision() or equivalent:

// Before — per request, O(N) hash work:
func (r *reverseProxy) hostByHashing(req *http.Request, pool UpstreamPool) *Upstream {
    key := r.hashKey(req)
    for _, upstream := range pool {
        h := xxhash.Sum64String(upstream.Dial + key)
        // select by h
    }
}

// After
// CWE-407 fix: pre-computed hash ring for O(log N) lookup instead of O(N) per-request hashing.
// At Provision() time:
type ringEntry struct {
    hash     uint64
    upstream *Upstream
}
// Build ring: for each upstream, compute xxhash(upstream.Dial), sort by hash.
r.ring = buildRing(pool)  // O(N log N) once at config load

// Per request:
func (r *reverseProxy) hostByHashing(req *http.Request) *Upstream {
    key := xxhash.Sum64String(r.hashKey(req))  // O(1)
    return r.ring.lookup(key)                  // O(log N) binary search
}

Patch

defects/caddy/patch/caddy-0001-reverseproxy-hash-ring.patch

What We Ask

  1. Confirm receipt and assign a GitHub Security Advisory or issue reference.
  2. Validate the patch against the reverse proxy load balancing test suite.
  3. Assess CVE eligibility — caddy-0001 fires on every request using consistent-hash routing.
  4. Coordinate a disclosure date — we are targeting 90 days from first contact.

Contact: see cover email. This brief is confidential until coordinated disclosure.