Skip to content

Commit a9d3477

Browse files
fix(gateway): enforce ms|s|m|h unit contract for upstream connect timeout
time.ParseDuration accepts units beyond the declared ms|s|m|h contract (ns, us, and compound durations like 1h30m), so an out-of-contract connect timeout could pass validation. Reject any value that is not a single ms|s|m|h duration as an invalid format, keeping the existing distinct invalid-format and non-positive messages.
1 parent 75c5c74 commit a9d3477

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

gateway/gateway-controller/pkg/config/api_validator.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type APIValidator struct {
3939
urlFriendlyNameRegex *regexp.Regexp
4040
// upstreamRefRegex enforces the schema pattern for per-op upstream refs
4141
upstreamRefRegex *regexp.Regexp
42+
// connectTimeoutRegex enforces the ms|s|m|h unit contract for upstream connect timeouts
43+
connectTimeoutRegex *regexp.Regexp
4244
// policyValidator validates policy references and parameters
4345
policyValidator *PolicyValidator
4446
}
@@ -50,6 +52,7 @@ func NewAPIValidator() *APIValidator {
5052
versionRegex: regexp.MustCompile(`^v?\d+(\.\d+)?(\.\d+)?$`),
5153
urlFriendlyNameRegex: regexp.MustCompile(`^[a-zA-Z0-9\-_\. ]+$`),
5254
upstreamRefRegex: regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`),
55+
connectTimeoutRegex: regexp.MustCompile(`^[+-]?\d+(\.\d+)?(ms|s|m|h)$`),
5356
}
5457
}
5558

@@ -383,6 +386,11 @@ func (v *APIValidator) validateUpstreamDefinitions(definitions *[]api.UpstreamDe
383386
Field: fmt.Sprintf("spec.upstreamDefinitions[%d].timeout.connect", i),
384387
Message: fmt.Sprintf("Invalid timeout format: %v (expected format: '30s', '1m', '500ms')", err),
385388
})
389+
} else if !v.connectTimeoutRegex.MatchString(timeoutStr) {
390+
errors = append(errors, ValidationError{
391+
Field: fmt.Sprintf("spec.upstreamDefinitions[%d].timeout.connect", i),
392+
Message: fmt.Sprintf("Invalid timeout format: %q (expected units: ms, s, m, h)", timeoutStr),
393+
})
386394
} else if d <= 0 {
387395
errors = append(errors, ValidationError{
388396
Field: fmt.Sprintf("spec.upstreamDefinitions[%d].timeout.connect", i),

gateway/gateway-controller/pkg/config/validator_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,35 @@ func TestValidateUpstreamDefinitions_MalformedTimeout(t *testing.T) {
867867
assert.Contains(t, errors[0].Message, "Invalid timeout format")
868868
}
869869

870+
func TestValidateUpstreamDefinitions_TimeoutUnitContract(t *testing.T) {
871+
validator := NewAPIValidator()
872+
873+
// time.ParseDuration accepts units outside the ms|s|m|h contract (ns, us, compounds);
874+
// these must be rejected as invalid format, not silently accepted.
875+
for _, badTimeout := range []string{"5ns", "100us", "1h30m"} {
876+
connect := badTimeout
877+
definitions := &[]api.UpstreamDefinition{
878+
{
879+
Name: "my-upstream",
880+
Timeout: &api.UpstreamTimeout{
881+
Connect: &connect,
882+
},
883+
Upstreams: []struct {
884+
Url string `json:"url" yaml:"url"`
885+
Weight *int `json:"weight,omitempty" yaml:"weight,omitempty"`
886+
}{
887+
{Url: "http://backend:8080"},
888+
},
889+
},
890+
}
891+
892+
errors := validator.validateUpstreamDefinitions(definitions)
893+
require.Len(t, errors, 1, "timeout %q must be rejected", badTimeout)
894+
assert.Equal(t, "spec.upstreamDefinitions[0].timeout.connect", errors[0].Field)
895+
assert.Contains(t, errors[0].Message, "Invalid timeout format")
896+
}
897+
}
898+
870899
func TestValidateUpstreamRef_ValidRef(t *testing.T) {
871900
validator := NewAPIValidator()
872901

0 commit comments

Comments
 (0)