mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-07 08:24:52 +00:00
fix(config): reject durations and sizes that overflow at startup
Several seconds settings became time.Duration with no upper bound, so a large value wrapped negative or to zero. maxTokenAgeSeconds then disabled the bearer age check, and a zero health check interval panicked time.NewTicker. A circuit breaker threshold above MaxInt32 narrowed to a value that tripped on the first failure. A huge poolSize allocated the connection channel up front. Validate now rejects these values. Sane and default values are unchanged.
This commit is contained in:
+44
@@ -659,6 +659,24 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("sessionMaxAge is too large; must be at most %d seconds", math.MaxInt64/int64(time.Second))
|
return fmt.Errorf("sessionMaxAge is too large; must be at most %d seconds", math.MaxInt64/int64(time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bearer durations are int64 seconds * time.Second at startup (main.go); an
|
||||||
|
// unbounded value wraps negative or to zero and disables the age check
|
||||||
|
// and failure throttle.
|
||||||
|
for name, v := range map[string]int64{
|
||||||
|
"maxTokenAgeSeconds": c.MaxTokenAgeSeconds,
|
||||||
|
"bearerFailureWindowSeconds": int64(c.BearerFailureWindowSeconds),
|
||||||
|
"bearerFailurePenaltySeconds": int64(c.BearerFailurePenaltySeconds),
|
||||||
|
} {
|
||||||
|
if v > maxDurationSeconds {
|
||||||
|
return fmt.Errorf("%s is too large; must be at most %d seconds", name, maxDurationSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The throttle sweep cutoff adds window and penalty.
|
||||||
|
if int64(c.BearerFailureWindowSeconds) > 0 && int64(c.BearerFailurePenaltySeconds) > 0 &&
|
||||||
|
int64(c.BearerFailureWindowSeconds)+int64(c.BearerFailurePenaltySeconds) > maxDurationSeconds {
|
||||||
|
return fmt.Errorf("bearerFailureWindowSeconds plus bearerFailurePenaltySeconds is too large")
|
||||||
|
}
|
||||||
|
|
||||||
// Validate audience if specified
|
// Validate audience if specified
|
||||||
if c.Audience != "" {
|
if c.Audience != "" {
|
||||||
// Validate audience format - should be a valid identifier or URL
|
// Validate audience format - should be a valid identifier or URL
|
||||||
@@ -1228,6 +1246,14 @@ func (c *Config) GetSecurityHeadersApplier() func(http.ResponseWriter, *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isOriginAllowed checks if an origin is in the allowed list
|
// isOriginAllowed checks if an origin is in the allowed list
|
||||||
|
const (
|
||||||
|
// maxDurationSeconds is the largest whole-second value whose
|
||||||
|
// time.Duration product with time.Second does not overflow int64.
|
||||||
|
maxDurationSeconds = math.MaxInt64 / int64(time.Second)
|
||||||
|
// maxRedisPoolSize bounds the eagerly allocated connection channel.
|
||||||
|
maxRedisPoolSize = 10000
|
||||||
|
)
|
||||||
|
|
||||||
// Validate checks if the Redis configuration is valid
|
// Validate checks if the Redis configuration is valid
|
||||||
func (rc *RedisConfig) Validate() error {
|
func (rc *RedisConfig) Validate() error {
|
||||||
if !rc.Enabled {
|
if !rc.Enabled {
|
||||||
@@ -1254,6 +1280,9 @@ func (rc *RedisConfig) Validate() error {
|
|||||||
if rc.PoolSize < 0 {
|
if rc.PoolSize < 0 {
|
||||||
return fmt.Errorf("pool size cannot be negative")
|
return fmt.Errorf("pool size cannot be negative")
|
||||||
}
|
}
|
||||||
|
if rc.PoolSize > maxRedisPoolSize {
|
||||||
|
return fmt.Errorf("pool size is too large; must be at most %d", maxRedisPoolSize)
|
||||||
|
}
|
||||||
if rc.ConnectTimeout < 0 {
|
if rc.ConnectTimeout < 0 {
|
||||||
return fmt.Errorf("connect timeout cannot be negative")
|
return fmt.Errorf("connect timeout cannot be negative")
|
||||||
}
|
}
|
||||||
@@ -1287,6 +1316,21 @@ func (rc *RedisConfig) Validate() error {
|
|||||||
return fmt.Errorf("health check interval cannot be negative")
|
return fmt.Errorf("health check interval cannot be negative")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seconds become time.Duration (x * time.Second) and the threshold is
|
||||||
|
// narrowed to int32 in the breaker; out-of-range values wrap, which
|
||||||
|
// panics time.NewTicker or trips the breaker on the first failure.
|
||||||
|
for name, v := range map[string]int{
|
||||||
|
"health check interval": rc.HealthCheckInterval,
|
||||||
|
"circuit breaker timeout": rc.CircuitBreakerTimeout,
|
||||||
|
} {
|
||||||
|
if int64(v) > maxDurationSeconds {
|
||||||
|
return fmt.Errorf("%s is too large; must be at most %d seconds", name, maxDurationSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rc.CircuitBreakerThreshold > math.MaxInt32 {
|
||||||
|
return fmt.Errorf("circuit breaker threshold is too large; must be at most %d", math.MaxInt32)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func boundsTestConfig() *Config {
|
||||||
|
cfg := CreateConfig()
|
||||||
|
cfg.ProviderURL = "https://auth.example.com"
|
||||||
|
cfg.CallbackURL = "/callback"
|
||||||
|
cfg.ClientID = "test-client"
|
||||||
|
cfg.ClientSecret = "test-client-secret"
|
||||||
|
cfg.SessionEncryptionKey = "test-encryption-key-32-bytes-long!!"
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWholeSeconds = math.MaxInt64 / int64(time.Second)
|
||||||
|
|
||||||
|
func TestConfigValidateRejectsDurationOverflow(t *testing.T) {
|
||||||
|
cases := map[string]func(c *Config){
|
||||||
|
"maxTokenAgeSeconds wraps negative": func(c *Config) { c.MaxTokenAgeSeconds = maxWholeSeconds + 1 },
|
||||||
|
"maxTokenAgeSeconds wraps to zero": func(c *Config) { c.MaxTokenAgeSeconds = 1 << 55 },
|
||||||
|
"bearerFailureWindowSeconds": func(c *Config) { c.BearerFailureWindowSeconds = int(maxWholeSeconds + 1) },
|
||||||
|
"bearerFailurePenaltySeconds": func(c *Config) { c.BearerFailurePenaltySeconds = int(maxWholeSeconds + 1) },
|
||||||
|
}
|
||||||
|
for name, mutate := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
cfg := boundsTestConfig()
|
||||||
|
mutate(cfg)
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatal("Validate accepted an overflowing duration")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("max exact value still valid", func(t *testing.T) {
|
||||||
|
cfg := boundsTestConfig()
|
||||||
|
cfg.MaxTokenAgeSeconds = maxWholeSeconds
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("boundary value rejected: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedisConfigValidateRejectsOverflowAndHugePool(t *testing.T) {
|
||||||
|
base := func() *RedisConfig {
|
||||||
|
rc := &RedisConfig{Enabled: true, Address: "localhost:6379"}
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
cases := map[string]func(rc *RedisConfig){
|
||||||
|
"healthCheckInterval wraps to zero": func(rc *RedisConfig) { rc.HealthCheckInterval = 1 << 55 },
|
||||||
|
"circuitBreakerTimeout wraps": func(rc *RedisConfig) { rc.CircuitBreakerTimeout = int(maxWholeSeconds + 1) },
|
||||||
|
"circuitBreakerThreshold narrows": func(rc *RedisConfig) { rc.CircuitBreakerThreshold = math.MaxInt32 + 1 },
|
||||||
|
"poolSize absurd": func(rc *RedisConfig) { rc.PoolSize = 1_000_000_000 },
|
||||||
|
}
|
||||||
|
for name, mutate := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
rc := base()
|
||||||
|
mutate(rc)
|
||||||
|
if err := rc.Validate(); err == nil {
|
||||||
|
t.Fatal("Validate accepted an unsafe value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("defaults and sane values stay valid", func(t *testing.T) {
|
||||||
|
rc := base()
|
||||||
|
rc.PoolSize, rc.CircuitBreakerThreshold, rc.HealthCheckInterval = 50, 5, 30
|
||||||
|
if err := rc.Validate(); err != nil {
|
||||||
|
t.Fatalf("sane config rejected: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user