From 4e8071aa2b6c8099b3c7e2960a187c15edf6af2c Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Wed, 4 Nov 2026 04:50:19 +0700 Subject: [PATCH] fix(observability-proxy): accept bare seconds only as plain digits Bare seconds were parsed with ParseFloat, which also takes signs, exponents, Inf and NaN. Such a start fell through to the relative duration form and silently moved the window, so 1.7937864e9 meant 1970 rather than an error, and NaN converted differently per CPU architecture. Only digits with an optional fraction are taken now; everything else is a 400. Assisted-by: LLM Signed-off-by: ZverGuy --- internal/observabilityproxy/logsparams.go | 9 ++++++--- internal/observabilityproxy/logsparams_test.go | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/internal/observabilityproxy/logsparams.go b/internal/observabilityproxy/logsparams.go index 653f1c194..c9fad47db 100644 --- a/internal/observabilityproxy/logsparams.go +++ b/internal/observabilityproxy/logsparams.go @@ -264,9 +264,12 @@ var durationUnits = map[string]time.Duration{ // parseDuration accepts the LogsQL duration forms (1h30m, 1d, 2w) and bare // seconds; Go's parser knows neither d nor w. func parseDuration(s string) (time.Duration, error) { - if f, err := strconv.ParseFloat(s, 64); err == nil { - if f < 0 { - return 0, fmt.Errorf("negative duration %q", s) + // Bare seconds only as digits with an optional fraction: ParseFloat + // would also take signs, exponents, Inf and NaN. + if whole, frac, ok := strings.Cut(s, "."); isDigits(whole) && (!ok || isDigits(frac)) { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, fmt.Errorf("invalid duration %q", s) } return time.Duration(f * float64(time.Second)), nil } diff --git a/internal/observabilityproxy/logsparams_test.go b/internal/observabilityproxy/logsparams_test.go index 8e6fb107d..abfbc3483 100644 --- a/internal/observabilityproxy/logsparams_test.go +++ b/internal/observabilityproxy/logsparams_test.go @@ -128,6 +128,14 @@ func TestBuildLogsParamsRejects(t *testing.T) { {"bad start", "query", url.Values{"start": {"yesterday"}}, http.StatusBadRequest}, {"millisecond timestamp", "query", url.Values{"start": {"1793786400000"}}, http.StatusBadRequest}, {"year-like number", "query", url.Values{"start": {"2026"}}, http.StatusBadRequest}, + {"exponent time", "query", url.Values{"start": {"1.7937864e9"}}, http.StatusBadRequest}, + {"signed time", "query", url.Values{"start": {"+1793786400"}}, http.StatusBadRequest}, + {"exponent duration", "query", url.Values{"start": {"3e2"}}, http.StatusBadRequest}, + {"trailing dot", "query", url.Values{"start": {"300."}}, http.StatusBadRequest}, + {"infinite time", "query", url.Values{"end": {"Inf"}}, http.StatusBadRequest}, + {"NaN time", "query", url.Values{"start": {"NaN"}}, http.StatusBadRequest}, + {"NaN step", "hits", url.Values{"step": {"NaN"}}, http.StatusBadRequest}, + {"NaN start_offset", "tail", url.Values{"start_offset": {"NaN"}}, http.StatusBadRequest}, {"bad limit", "query", url.Values{"limit": {"-1"}}, http.StatusBadRequest}, {"zero limit", "query", url.Values{"limit": {"0"}}, http.StatusBadRequest}, {"bad step", "hits", url.Values{"step": {"0s"}}, http.StatusBadRequest},