Files
traefikoidc/review_r89_test.go
T
lukaszraczylo d0ec853499 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).
2026-08-13 19:34:38 +01:00

65 lines
2.6 KiB
Go

package traefikoidc
import (
"net/http"
"testing"
)
// R89 regression: pathExcluded matched the RAW request path as a string
// prefix. Go's server leaves ".." in req.URL.Path (url.ParseRequestURI does
// not normalize dot segments), so an exclusion for "/public" also matched
// "/public/../admin", letting a dot-segment traversal reach a sibling
// resource (e.g. /admin) unauthenticated. The matcher must normalize the
// request path (path.Clean) before the boundary comparison so traversal
// cannot widen an exclusion into an authentication bypass.
func TestR89ExcludedPathDotTraversal(t *testing.T) {
// Exact-matching and legitimate-subtree exclusions must be preserved.
if !pathExcluded("/public", "/public") {
t.Error("exact match should be excluded")
}
if !pathExcluded("/public/page", "/public") {
t.Error("sub-path should still be excluded")
}
if !pathExcluded("/public/./page", "/public") {
t.Error("un-escaped ./ inside the excluded subtree should still be excluded")
}
// Dot-segment traversal must NOT escape INTO the exclusion namespace and
// thereby reach a sibling resource unauthenticated.
if pathExcluded("/public/../admin", "/public") {
t.Error("excluded /public must not also cover /admin reached via ../ (auth bypass)")
}
if pathExcluded("/public/..", "/public") {
t.Error("excluded /public must not cover root reached via ..")
}
if pathExcluded("/public/../", "/public") {
t.Error("excluded /public must not cover the bare root reached via ../")
}
if pathExcluded("/public/x/../../admin", "/public") {
t.Error("nested traversal must not escape the subtree into /admin")
}
}
// R89 regression: CreatePooledHTTPClient / GetOrCreateTransport must apply the
// same zero-value defaults as CreateHTTPClient. Before, a partially populated
// config (e.g. only {Timeout: 10s}, or zero) produced an http.Client with
// Timeout == 0 and a transport with ResponseHeaderTimeout == 0 — an unbounded
// stall on a hanging upstream. Both must now be bounded by the shared
// defaults.
func TestR89PooledClientAppliesDefaults(t *testing.T) {
client := CreatePooledHTTPClient(HTTPClientConfig{})
if client.Timeout == 0 {
t.Error("pooled HTTP client must carry a default overall Timeout; zero = unbounded stall")
}
tr, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("pooled client transport is %T, want *http.Transport", client.Transport)
}
if tr.ResponseHeaderTimeout == 0 {
t.Error("pooled transport must carry a default ResponseHeaderTimeout; zero = unbounded stall")
}
if tr.IdleConnTimeout == 0 {
t.Error("pooled transport must carry a default IdleConnTimeout")
}
}