From 15eec1e5b198a92ef614bf0d14a555d01730fc6d Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Thu, 5 Nov 2026 02:11:50 +0700 Subject: [PATCH] fix(observability-proxy): refuse logs times vlselect cannot hold The proxy sends vlselect RFC3339 times, which it never reads as a guessed-unit number, but it accepted any RFC3339 year and unix seconds up to 2286. vlselect keeps times as int64 nanoseconds through time.UnixNano, which is undefined outside 1678..2262, so such a window landed anywhere. An absolute time outside 1970..2262 now answers 400; nothing was logged before 1970. Assisted-by: LLM Signed-off-by: ZverGuy --- internal/observabilityproxy/logsparams.go | 17 +++++++++++++++-- internal/observabilityproxy/logsparams_test.go | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/observabilityproxy/logsparams.go b/internal/observabilityproxy/logsparams.go index 93e1bd9a4..c3e8170c5 100644 --- a/internal/observabilityproxy/logsparams.go +++ b/internal/observabilityproxy/logsparams.go @@ -325,6 +325,19 @@ func scaleDuration(f float64, unit time.Duration) (time.Duration, bool) { // and a duration meaning that long before now (VictoriaLogs' "start=3h"). // Anything else is refused rather than guessed, since a misread timestamp // silently moves the window. +// maxLogsTime is the last instant int64 nanoseconds hold. +var maxLogsTime = time.Unix(0, math.MaxInt64) + +// absoluteLogsTime refuses a time vlselect cannot hold: it keeps times as +// int64 nanoseconds (time.UnixNano), which is undefined outside 1678..2262 +// and would move the window anywhere; nothing was logged before 1970. +func absoluteLogsTime(s string, t time.Time) (time.Time, error) { + if t.Before(time.Unix(0, 0)) || !t.Before(maxLogsTime) { + return time.Time{}, fmt.Errorf("time %q is outside 1970..2262", s) + } + return t, nil +} + func parseTime(s string, now time.Time) (time.Time, error) { if s == "now" { return now, nil @@ -344,10 +357,10 @@ func parseTime(s string, now time.Time) (time.Time, error) { frac = (frac + "000000000")[:9] nanos, _ = strconv.ParseInt(frac, 10, 64) } - return time.Unix(secs, nanos), nil + return absoluteLogsTime(s, time.Unix(secs, nanos)) } if t, err := time.Parse(time.RFC3339Nano, s); err == nil { - return t, nil + return absoluteLogsTime(s, t) } if d, err := parseDuration(s); err == nil { return now.Add(-d), nil diff --git a/internal/observabilityproxy/logsparams_test.go b/internal/observabilityproxy/logsparams_test.go index 009b1721d..c5cc9d4c5 100644 --- a/internal/observabilityproxy/logsparams_test.go +++ b/internal/observabilityproxy/logsparams_test.go @@ -135,6 +135,12 @@ func TestBuildLogsParamsRejects(t *testing.T) { {"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}, + // vlselect keeps times as int64 nanoseconds (time.UnixNano), which + // overflows outside 1678..2262; before 1970 is no log time either. + {"time before 1970", "query", url.Values{"start": {"0001-01-01T00:00:00Z"}, "end": {"0001-01-02T00:00:00Z"}}, http.StatusBadRequest}, + {"time past int64 nanoseconds", "query", url.Values{"start": {"9999-01-01T00:00:00Z"}, "end": {"9999-01-02T00:00:00Z"}}, http.StatusBadRequest}, + {"tail since before 1970", "tail", url.Values{"since": {"1969-12-31T23:59:59Z"}}, http.StatusBadRequest}, + {"unix seconds past int64 nanoseconds", "query", url.Values{"start": {"9999999990"}, "end": {"9999999999"}}, 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},