R89: normalize excluded-url paths; apply defaults to pooled HTTP clients

Two verified defects from R89 scout round:

1. Excluded-URL dot-segment traversal widening (P1, auth bypass).
   shouldBypassAuth passes req.URL.Path raw (middleware.go:54); Go's server
   leaves '..' in URL.Path (url.ParseRequestURI does not normalize), so an
   exclusion for /public also matched /public/../admin, letting a real
   sibling (/admin) reach the backend unauthenticated. pathExcluded now
   path.Clean()s the request path ('.' -> '' to preserve root semantics)
   before boundary matching. Regression TestR89ExcludedPathDotTraversal
   fails on old (all 4 traversal cases wrongly excluded), passes on new.

2. Pooled HTTP client had no zero-value defaults (P2, unbounded stall).
   CreateHTTPClient applies defaults; CreatePooledHTTPClient and
   GetOrCreateTransport did not, so a partial config yielded Timeout=0 and
   ResponseHeaderTimeout=0 (a hanging upstream stalls forever). Extracted
   applyHTTPClientDefaults (single source of truth) and applied it in all
   three construction paths; pool cache key now reflects normalized values.
   Regression TestR89PooledClientAppliesDefaults fails on old, passes on new.

R89 clean/declined: session max-age IS absolute-and-enforced-every-request
(verified, review_r68 already locks it); AllowedUserDomains fail-closed (P2
'*' not a wildcard, P3 TrimSpace — declined as config-scoped).
This commit is contained in:
2026-08-13 19:34:38 +01:00
parent 866e57380d
commit d0ec853499
4 changed files with 98 additions and 4 deletions
+9
View File
@@ -58,6 +58,12 @@ func GetGlobalTransportPool() *SharedTransportPool {
// GetOrCreateTransport gets or creates a shared transport with the given config
func (p *SharedTransportPool) GetOrCreateTransport(config HTTPClientConfig) *http.Transport {
// Apply the same zero-value defaults as CreateHTTPClient so a partially
// populated config never yields an unbounded client/transport, and so the
// pool cache key reflects normalized values (configKey). Bounded here,
// before the client-count gate, so both the gate and the key see
// normalized timeouts.
applyHTTPClientDefaults(&config)
// SECURITY FIX: Check client limit before creating new transport.
if atomic.LoadInt32(&p.clientCount) >= p.maxClients {
// At the client limit: only reuse a transport that was built for the
@@ -282,6 +288,9 @@ func (p *SharedTransportPool) Cleanup() {
// CreatePooledHTTPClient creates an HTTP client using the shared transport pool
func CreatePooledHTTPClient(config HTTPClientConfig) *http.Client {
// Apply the same zero-value defaults as CreateHTTPClient so a partially
// populated config still gets a bounded overall request Timeout.
applyHTTPClientDefaults(&config)
pool := GetGlobalTransportPool()
transport := pool.GetOrCreateTransport(config)