From 97a536b89adcf7ba42ef7c35bef0ca80a2638fa2 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Wed, 4 Nov 2026 05:24:04 +0700 Subject: [PATCH] fix(observability-proxy): refuse durations past the int64 range parseDuration summed duration parts without an overflow check and converted out-of-range floats to Duration, which Go leaves implementation-defined. A start_offset such as 99999999999h99999999999h1s wrapped to 999ms and was accepted, and a huge step saturated on some architectures and wrapped on others. Such values now answer 400 like any other invalid duration. Assisted-by: LLM Signed-off-by: ZverGuy --- internal/observabilityproxy/logsparams.go | 24 +++++++++++++++++-- .../observabilityproxy/logsparams_test.go | 18 ++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/internal/observabilityproxy/logsparams.go b/internal/observabilityproxy/logsparams.go index 7be252998..520f89801 100644 --- a/internal/observabilityproxy/logsparams.go +++ b/internal/observabilityproxy/logsparams.go @@ -18,6 +18,7 @@ package observabilityproxy import ( "fmt" + "math" "net/http" "net/url" "regexp" @@ -275,7 +276,11 @@ func parseDuration(s string) (time.Duration, error) { if err != nil { return 0, fmt.Errorf("invalid duration %q", s) } - return time.Duration(f * float64(time.Second)), nil + d, ok := scaleDuration(f, time.Second) + if !ok { + return 0, fmt.Errorf("duration %q out of range", s) + } + return d, nil } if s == "" { return 0, fmt.Errorf("empty duration") @@ -287,12 +292,27 @@ func parseDuration(s string) (time.Duration, error) { return 0, fmt.Errorf("invalid duration %q", s) } f, _ := strconv.ParseFloat(m[1], 64) - total += time.Duration(f * float64(durationUnits[m[2]])) + d, ok := scaleDuration(f, durationUnits[m[2]]) + if !ok || total > math.MaxInt64-d { + return 0, fmt.Errorf("duration %q out of range", s) + } + total += d rest = rest[len(m[0]):] } return total, nil } +// scaleDuration refuses what does not fit a Duration: Go leaves an +// out-of-range float to int conversion implementation-defined. +func scaleDuration(f float64, unit time.Duration) (time.Duration, bool) { + v := f * float64(unit) + // float64(math.MaxInt64) rounds up to 2^63, which is already too large. + if v >= float64(math.MaxInt64) { + return 0, false + } + return time.Duration(v), true +} + // parseTime accepts "now", unix seconds with an optional fraction, RFC3339, // and a duration meaning that long before now (VictoriaLogs' "start=3h"). // Anything else is refused rather than guessed, since a misread timestamp diff --git a/internal/observabilityproxy/logsparams_test.go b/internal/observabilityproxy/logsparams_test.go index ddb95cdc5..46c62ffb9 100644 --- a/internal/observabilityproxy/logsparams_test.go +++ b/internal/observabilityproxy/logsparams_test.go @@ -143,6 +143,11 @@ func TestBuildLogsParamsRejects(t *testing.T) { {"too many hits fields", "hits", url.Values{"field": strings.Fields("a b c d e f g h i j k")}, http.StatusBadRequest}, {"since and start_offset", "tail", url.Values{"since": {"2026-11-04T11:59:00Z"}, "start_offset": {"1m"}}, http.StatusBadRequest}, {"bad since", "tail", url.Values{"since": {"soon"}}, http.StatusBadRequest}, + {"start_offset summing past int64", "tail", url.Values{"start_offset": {"99999999999h99999999999h1s"}}, http.StatusBadRequest}, + {"step summing past int64", "hits", url.Values{"step": {"2562047h2562047h"}}, http.StatusBadRequest}, + {"step token past int64", "hits", url.Values{"step": {"99999999999h"}}, http.StatusBadRequest}, + {"bare-seconds step past int64", "hits", url.Values{"step": {"99999999999999999999"}}, http.StatusBadRequest}, + {"relative start summing past int64", "query", url.Values{"start": {"106751d1d"}}, http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -352,3 +357,16 @@ func TestBuildLogsParamsStepKeepsBucketCap(t *testing.T) { } } } + +func TestParseDurationBounds(t *testing.T) { + for _, in := range []string{"2562047h", "106751d", "9223372036s", "292y"} { + if d, err := parseDuration(in); err != nil || d <= 0 { + t.Errorf("parseDuration(%q) = %s, %v; want a positive duration", in, d, err) + } + } + for _, in := range []string{"2562048h", "106751d1d", "9223372037s", "9223372037", "293y", "1y292y"} { + if d, err := parseDuration(in); err == nil { + t.Errorf("parseDuration(%q) = %s; want an error past the int64 range", in, d) + } + } +}