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 <maximbel2003@gmail.com>
This commit is contained in:
ZverGuy committed 2026-11-10 00:45:06 +07:00
1 parent 889712d244
commit 97a536b89a
2 files changed
+40 -2

No files matched your search

+22 -2
View File
@@ -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
@@ -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)
}
}
}