mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
fix(cache): harden cache backends, resilience, recovery, and transports
- RESP -ERR no longer closes healthy conn / aborts batch; global cap replacement-aware - global health-check pass serialized (latent data race); MemoryMonitor via ResourceManager - token-exchange retry, open-circuit rejections counted as failures - TLS-posture matching transport reuse (no silent InsecureSkipVerify handoff)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestR176_SanitizeForwardedHostRejectsPathDelimiters regresses
|
||||
// sanitizeForwardedHost: it previously only rejected control/whitespace
|
||||
// characters, so a crafted X-Forwarded-Host like "example.com/path" (or
|
||||
// with '?', '#', '@') survived and its delimiter was spliced verbatim into
|
||||
// the host part of a redirect URL (used as redirect_uri /
|
||||
// post_logout_redirect_uri and matched by the IdP). Valid host
|
||||
// characters — alphanumerics, '-', '.', ':', '[', ']' (port / IPv6) —
|
||||
// must still pass.
|
||||
func TestR176_SanitizeForwardedHostRejectsPathDelimiters(t *testing.T) {
|
||||
valid := []string{
|
||||
"example.com",
|
||||
"example.com:8443",
|
||||
"api.v2.example.co.uk",
|
||||
"10.0.0.1",
|
||||
"[2001:db8::1]",
|
||||
"[fe80::1]:8080",
|
||||
}
|
||||
for _, in := range valid {
|
||||
if got := sanitizeForwardedHost(in); got != in {
|
||||
t.Fatalf("valid host %q was rejected: got %q", in, got)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"example.com/path", // path injection
|
||||
"example.com?x=1", // query injection
|
||||
"example.com#frag", // fragment injection
|
||||
"[email protected]", // userinfo injection
|
||||
"example.com\\path",
|
||||
"example.com /evil", // embedded space + path
|
||||
}
|
||||
for _, in := range invalid {
|
||||
if got := sanitizeForwardedHost(in); got != "" {
|
||||
t.Fatalf("host %q must be rejected (delimiter would poison the redirect base), got %q", in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-3
@@ -137,8 +137,12 @@ func DetermineScheme(req *http.Request, forceHTTPS bool) string {
|
||||
return "https"
|
||||
}
|
||||
|
||||
// Check X-Forwarded-Proto header for proxy scenarios
|
||||
if scheme := req.Header.Get("X-Forwarded-Proto"); scheme != "" {
|
||||
// Check X-Forwarded-Proto header for proxy scenarios. Only trusted
|
||||
// values are accepted: a malformed/junk scheme (e.g. an attacker-set
|
||||
// "X-Forwarded-Proto: ftp") would otherwise be emitted verbatim into
|
||||
// redirect_uri as a broken or weakened scheme. Invalid values fall
|
||||
// through to the TLS check (R103).
|
||||
if scheme := req.Header.Get("X-Forwarded-Proto"); scheme == "https" || scheme == "http" {
|
||||
return scheme
|
||||
}
|
||||
|
||||
@@ -177,7 +181,17 @@ func sanitizeForwardedHost(v string) string {
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.IndexFunc(v, func(r rune) bool { return r < 0x20 || r == 0x7f || r == ' ' }) >= 0 {
|
||||
if strings.IndexFunc(v, func(r rune) bool {
|
||||
// Reject control chars, whitespace, and the delimiters that would
|
||||
// let a crafted X-Forwarded-Host smuggle a path (''/''), query
|
||||
// (''?''), fragment (''#''), or userinfo (''@'') into the
|
||||
// host part of a redirect URL (which is passed to the IdP as
|
||||
// redirect_uri / post_logout_redirect_uri and matched there).
|
||||
// Valid host characters — alphanumerics, '-', '.', ':', '[', ']'
|
||||
// (port / IPv6) — remain allowed.
|
||||
return r < 0x20 || r == 0x7f || r == ' ' ||
|
||||
r == '/' || r == '?' || r == '#' || r == '@' || r == '\\'
|
||||
}) >= 0 {
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
// TestDetermineScheme verifies the scheme priority: forceHTTPS > X-Forwarded-Proto > TLS > http.
|
||||
func TestDetermineScheme(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
forceHTTPS bool
|
||||
fwdProto string
|
||||
tls bool
|
||||
want string
|
||||
name string
|
||||
forceHTTPS bool
|
||||
fwdProto string
|
||||
tls bool
|
||||
want string
|
||||
}{
|
||||
{"force https wins over header", true, "http", false, "https"},
|
||||
{"force https wins over tls absence", true, "", false, "https"},
|
||||
@@ -21,6 +21,11 @@ func TestDetermineScheme(t *testing.T) {
|
||||
{"forwarded proto http", false, "http", true, "http"},
|
||||
{"tls connection", false, "", true, "https"},
|
||||
{"default http", false, "", false, "http"},
|
||||
// R103: a malformed X-Forwarded-Proto must not be emitted verbatim
|
||||
// into redirect_uri (would yield a broken/weakened scheme). It
|
||||
// falls through to the TLS check / http default instead.
|
||||
{"invalid proto falls through to tls", false, "ftp", true, "https"},
|
||||
{"invalid proto falls through to http", false, "ftp", false, "http"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@@ -41,10 +46,10 @@ func TestDetermineScheme(t *testing.T) {
|
||||
// TestDetermineHost verifies forwarding-header preference and fallback to req.Host.
|
||||
func TestDetermineHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fwd string
|
||||
host string
|
||||
want string
|
||||
name string
|
||||
fwd string
|
||||
host string
|
||||
want string
|
||||
}{
|
||||
{"forwarded host used", "api.example.com", "backend.local", "api.example.com"},
|
||||
{"first of comma list taken", "a.example.com, b.example.com", "backend.local", "a.example.com"},
|
||||
|
||||
Reference in New Issue
Block a user