Integer/Time parsing overflow (input validation for time parsing)

MEDIUM
victoriametrics/victoriametrics
Commit: 0c9a011e0a3b
Affected: <= 1.139.0 (prior to this fix)
2026-05-22 12:16 UTC

Description

This commit adds explicit overflow checks in time parsing to prevent int64 overflow when computing nanoseconds for relative times. Specifically, ParseTimeAt now computes nsec := currentTimestamp + int64(d) and if nsec < 0 it returns an error indicating the value is outside the allowed time range, instead of allowing wraparound. Additionally, relative duration parsing in ParseDuration is bounded by min/max valid values and returns an error when the parsed duration is outside those limits. These changes tighten input validation for time parsing, reducing the risk of negative or out-of-range timestamps being produced from crafted inputs, which could otherwise lead to incorrect time calculations, crashes, or logic errors in time-dependent components.

Proof of Concept

Proof-of-concept (Go): - Scenario 1: Trigger overflow with an extremely large numeric input for a time value (MaxInt64+1) that would overflow when added to the current timestamp. This demonstrates the path that previously could wrap to an invalid or unintended time. Code (example): package main import ( "fmt" tutil "github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil" ) func main() { // Choose an arbitrary current timestamp (in nanoseconds) now := int64(1<<60) // a large value to illustrate overflow risk // This string encodes a value that, when parsed by the relative-time logic, // would be combined with 'now' and could overflow a signed 64-bit int. // In the real parser, this would typically come from user input (e.g., API param). input := "9223372036854775808" // MaxInt64+1 as decimal if v, err := tutil.ParseTimeAt(input, now); err != nil { fmt.Println("expected error (overflow):", err) } else { fmt.Println("parsed time:", v, "(UTC)") } } Notes: - A successful exploit would craft an input that, when parsed as a relative time, causes currentTimestamp + int64(d) to overflow or become negative. With the fix, such inputs now return an error instead of producing an out-of-range or wrapped timestamp. - This PoC relies on the library path used by VictoriaMetrics (lib/timeutil). Adapt import path as needed for your build environment. - Scenario 2 (edge-case before fix): Use a value that resolves to a timestamp before 1970 (e.g., a string like "-9223372036.854" or similar large negative relative value). In older behavior this could produce a negative timestamp without proper validation; the fix guards against this by returning an error when nsec < 0.

Commit Details

Author: andriibeee

Date: 2026-05-22 10:17 UTC

Message:

lib/timeutil: handle int64 overflow in relative duration parsing (#10883) Fix `int64` overflow in `ParseTimeAt()` in `lib/timeutil/time.go` when converting time expressed as string into nanoseconds. Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10880. --------- Signed-off-by: Andrii Beskomornyi <andriibeee@gmail.com> Co-authored-by: Artem Fetishev <rtm@victoriametrics.com>

Triage Assessment

Vulnerability Type: Input Validation / Time parsing overflow

Confidence: MEDIUM

Reasoning:

The changes add explicit overflow checks when parsing times and durations, preventing potential negative nanosecond results and out-of-range timestamps. This tightens input validation during time parsing, which can mitigate edge-case exploits or crashes caused by overflow, a form of input handling vulnerability.

Verification Assessment

Vulnerability Type: Integer/Time parsing overflow (input validation for time parsing)

Confidence: MEDIUM

Affected Versions: <= 1.139.0 (prior to this fix)

Code Diff

diff --git a/app/vmctl/vmctlutil/time_test.go b/app/vmctl/vmctlutil/time_test.go index fa2b996377333..52cefeb18b679 100644 --- a/app/vmctl/vmctlutil/time_test.go +++ b/app/vmctl/vmctlutil/time_test.go @@ -20,6 +20,9 @@ func TestGetTime_Failure(t *testing.T) { // negative time f("-292273086-05-16T16:47:06Z") + + // relative duration that resolves to a timestamp before 1970 + f("-9223372036.855") } func TestGetTime_Success(t *testing.T) { @@ -77,9 +80,6 @@ func TestGetTime_Success(t *testing.T) { // float timestamp representation", f("1562529662.324", time.Date(2019, 7, 7, 20, 01, 02, 324e6, time.UTC)) - // negative timestamp - f("-9223372036.855", time.Date(1970, 01, 01, 00, 00, 00, 00, time.UTC)) - // big timestamp f("1223372036855", time.Date(2008, 10, 7, 9, 33, 56, 855e6, time.UTC)) diff --git a/lib/httputil/time_test.go b/lib/httputil/time_test.go index 1b536d39dccbd..e037ff4f0cbbb 100644 --- a/lib/httputil/time_test.go +++ b/lib/httputil/time_test.go @@ -51,8 +51,6 @@ func TestGetTimeSuccess(t *testing.T) { f("-292273086-05-16T16:47:06Z", minTimeMsecs) f("292277025-08-18T07:12:54.999999999Z", maxTimeMsecs) f("1562529662.324", 1562529662324) - f("-9223372036.854", minTimeMsecs) - f("-9223372036.855", minTimeMsecs) f("1223372036.855", 1223372036855) } @@ -85,4 +83,8 @@ func TestGetTimeError(t *testing.T) { f("292277025-08-18T07:12:54.999999998Z") f("123md") f("-12.3md") + + // relative duration that resolves to a timestamp before 1970 + f("-9223372036.854") + f("-9223372036.855") } diff --git a/lib/timeutil/duration.go b/lib/timeutil/duration.go index c60133cb6985a..24949481d7cdd 100644 --- a/lib/timeutil/duration.go +++ b/lib/timeutil/duration.go @@ -1,16 +1,25 @@ package timeutil import ( + "fmt" "time" "github.com/VictoriaMetrics/metricsql" ) +var ( + minDuration = time.Duration(minValidMilli * time.Millisecond) + maxDuration = time.Duration(maxValidMilli * time.Millisecond) +) + // ParseDuration parses duration string in Prometheus format func ParseDuration(s string) (time.Duration, error) { ms, err := metricsql.DurationValue(s, 0) if err != nil { return 0, err } + if ms < minValidMilli || maxValidMilli < ms { + return 0, fmt.Errorf("duration %q must be in the range [%v, %v]", s, minDuration, maxDuration) + } return time.Duration(ms) * time.Millisecond, nil } diff --git a/lib/timeutil/duration_test.go b/lib/timeutil/duration_test.go index 030df32429953..27cfbbe0cd0cf 100644 --- a/lib/timeutil/duration_test.go +++ b/lib/timeutil/duration_test.go @@ -1,6 +1,7 @@ package timeutil import ( + "fmt" "testing" "time" ) @@ -27,3 +28,126 @@ func TestParseDuration(t *testing.T) { f("-1m30s", -(time.Minute + time.Second*30)) f("1d-4h", time.Hour*20) } + +func TestParseDurationLimits(t *testing.T) { + f := func(s string, want time.Duration) { + t.Helper() + got, err := ParseDuration(s) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != want { + t.Fatalf("unexpected result: got %v, want %v", got, want) + } + } + + var s string + var want time.Duration + + s = fmt.Sprintf("%dms", int64(minValidMilli)) + f(s, minDuration) + s = fmt.Sprintf("%dms", int64(maxValidMilli)) + f(s, maxDuration) + + s = fmt.Sprintf("%ds", int64(minValidSecond)) + want = minValidSecond * time.Second + f(s, want) + s = fmt.Sprintf("%ds", int64(maxValidSecond)) + want = maxValidSecond * time.Second + f(s, want) + + // When no unit is specified, seconds are assumed. + s = fmt.Sprintf("%d", int64(minValidSecond)) + want = minValidSecond * time.Second + f(s, want) + s = fmt.Sprintf("%d", int64(maxValidSecond)) + want = maxValidSecond * time.Second + f(s, want) + + minValidMinute := int64(minValidSecond) / 60 + maxValidMinute := int64(maxValidSecond) / 60 + s = fmt.Sprintf("%dm", minValidMinute) + want = time.Duration(minValidMinute) * time.Minute + f(s, want) + s = fmt.Sprintf("%dm", maxValidMinute) + want = time.Duration(maxValidMinute) * time.Minute + f(s, want) + + minValidHour := minValidMinute / 60 + maxValidHour := maxValidMinute / 60 + s = fmt.Sprintf("%dh", minValidHour) + want = time.Duration(minValidHour) * time.Hour + f(s, want) + s = fmt.Sprintf("%dh", maxValidHour) + want = time.Duration(maxValidHour) * time.Hour + f(s, want) + + minValidDay := minValidHour / 24 + maxValidDay := maxValidHour / 24 + s = fmt.Sprintf("%dd", minValidDay) + want = time.Duration(minValidDay) * 24 * time.Hour + f(s, want) + s = fmt.Sprintf("%dd", maxValidDay) + want = time.Duration(maxValidDay) * 24 * time.Hour + f(s, want) + + minValidWeek := minValidDay / 7 + maxValidWeek := maxValidDay / 7 + s = fmt.Sprintf("%dw", minValidWeek) + want = time.Duration(minValidWeek) * 7 * 24 * time.Hour + f(s, want) + s = fmt.Sprintf("%dw", maxValidWeek) + want = time.Duration(maxValidWeek) * 7 * 24 * time.Hour + f(s, want) + + minValidYear := minValidDay / 365 + maxValidYear := maxValidDay / 365 + s = fmt.Sprintf("%dy", minValidYear) + want = time.Duration(minValidYear) * 365 * 24 * time.Hour + f(s, want) + s = fmt.Sprintf("%dy", maxValidYear) + want = time.Duration(maxValidYear) * 365 * 24 * time.Hour + f(s, want) +} + +func TestParseDurationOutsideLimits(t *testing.T) { + f := func(s string) { + t.Helper() + got, err := ParseDuration(s) + gotDuration := time.Duration(got) * time.Millisecond + if err == nil { + t.Fatalf("ParseDuration(%s) unexpected result: got %d (%s), want error", s, got, gotDuration) + } + } + + f(fmt.Sprintf("%dms", int64(minValidMilli)-1)) + f(fmt.Sprintf("%dms", int64(maxValidMilli)+1)) + + f(fmt.Sprintf("%ds", int64(minValidSecond)-1)) + f(fmt.Sprintf("%ds", int64(maxValidSecond)+1)) + + minValidMinute := int64(minValidSecond)/60 - 1 + f(fmt.Sprintf("%dm", minValidMinute)) + maxValidMinute := int64(maxValidSecond)/60 + 1 + f(fmt.Sprintf("%dm", maxValidMinute)) + + minValidHour := minValidMinute/60 - 1 + f(fmt.Sprintf("%dh", minValidHour)) + maxValidHour := maxValidMinute/60 + 2 + f(fmt.Sprintf("%dh", maxValidHour)) + + minValidDay := minValidHour/24 - 1 + f(fmt.Sprintf("%dd", minValidDay)) + maxValidDay := maxValidHour/24 + 1 + f(fmt.Sprintf("%dd", maxValidDay)) + + minValidWeek := minValidDay/7 - 1 + f(fmt.Sprintf("%dw", minValidWeek)) + maxValidWeek := maxValidDay/7 + 1 + f(fmt.Sprintf("%dw", maxValidWeek)) + + minValidYear := minValidDay/365 - 1 + f(fmt.Sprintf("%dy", minValidYear)) + maxValidYear := maxValidDay/365 + 1 + f(fmt.Sprintf("%dy", maxValidYear)) +} diff --git a/lib/timeutil/time.go b/lib/timeutil/time.go index 7dc205ebc0fea..b84c46c571367 100644 --- a/lib/timeutil/time.go +++ b/lib/timeutil/time.go @@ -74,7 +74,11 @@ func ParseTimeAt(s string, currentTimestamp int64) (int64, error) { if d > 0 { d = -d } - return currentTimestamp + int64(d), nil + nsec := currentTimestamp + int64(d) + if nsec < 0 { + return 0, fmt.Errorf("time %s (%v) must be in the range [%v, %v]", sOrig, time.Unix(0, nsec).UTC(), minTime, maxTime) + } + return nsec, nil } if len(s) == 4 { // Parse YYYY diff --git a/lib/timeutil/time_test.go b/lib/timeutil/time_test.go index ad2f9ef8cc6df..0714e168e0398 100644 --- a/lib/timeutil/time_test.go +++ b/lib/timeutil/time_test.go @@ -1,6 +1,8 @@ package timeutil import ( + "fmt" + "math" "strings" "testing" "time" @@ -204,10 +206,11 @@ func TestParseTimeAtSuccess(t *testing.T) { } func TestParseTimeAtLimits(t *testing.T) { + now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + f := func(s string, wantTime time.Time) { t.Helper() - currentTimestamp := time.Now().UnixNano() - got, err := ParseTimeAt(s, currentTimestamp) + got, err := ParseTimeAt(s, now.UnixNano()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -226,6 +229,14 @@ func TestParseTimeAtLimits(t *testing.T) { } east := location(t, "Etc/GMT-14") // UTC+14:00 west := location(t, "Etc/GMT+12") // UTC-12:00 + var s string + + // min timestamp + f("0", time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)) + s = fmt.Sprintf("-%d", now.Unix()) + f(s, time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)) + s = fmt.Sprintf("now-%d", now.Unix()) + f(s, time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)) // min year f("1970Z", time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)) @@ -286,13 +297,39 @@ func TestParseTimeAtLimits(t *testing.T) { f("2262-04-11T23:47:16Z", time.Date(2262, 4, 11, 23, 47, 16, 0, time.UTC)) f("2262-04-12T13:47:16+14:00", time.Date(2262, 4, 12, 13, 47, 16, 0, east)) f("2262-04-11T11:47:16-12:00", time.Date(2262, 4, 11, 11, 47, 16, 0, west)) + + // max timestamp + s = fmt.Sprintf("%d", int64(maxValidSecond)) + f(s, time.Date(2262, 4, 11, 23, 47, 16, 0, time.UTC)) + s = fmt.Sprintf("%d", int64(maxValidMilli)) + f(s, time.Date(2262, 4, 11, 23, 47, 16, 854_000_000, time.UTC)) + s = fmt.Sprintf("%d", int64(maxValidMicro)) + f(s, time.Date(2262, 4, 11, 23, 47, 16, 854_775_000, time.UTC)) + s = fmt.Sprintf("%d", int64(math.MaxInt64)) + f(s, time.Date(2262, 4, 11, 23, 47, 16, 854_775_807, time.UTC)) + + // timestamps beyond max valid second are still valid but are treated as + // milliseconds. + s = fmt.Sprintf("%d", int64(maxValidSecond)+1) + f(s, time.Date(1970, 4, 17, 18, 2, 52, 37_000_000, time.UTC)) + + // timestamps beyond max valid millisecond are still valid but are treated + // as microseconds. + s = fmt.Sprintf("%d", int64(maxValidMilli)+1) + f(s, time.Date(1970, 4, 17, 18, 2, 52, 36_855_000, time.UTC)) + + // timestamps beyond max valid microsecond are still valid but are treated + // as nanoseconds. + s = fmt.Sprintf("%d", int64(maxValidMicro)+1) + f(s, time.Date(1970, 4, 17, 18, 2, 52, 36_854_776, time.UTC)) } func TestParseTimeAtOutsideLimits(t *testing.T) { + now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + f := func(s string) { t.Helper() - currentTimestamp := time.Now().UnixNano() - got, err := ParseTimeAt(s, currentTimestamp) + got, err := ParseTimeAt(s, now.UnixNano()) if err == nil { t.Fatalf("expected error but got %d", got) } @@ -301,6 +338,10 @@ func TestParseTimeAtOutsideLimits(t *testing.T) { } } + // min timestamp + f(fmt.Sprintf("-%d", now.Unix()+1)) + f(fmt.Sprintf("now-%d", now.Unix()+1)) + // min year f("1969Z") f("1970+14:00") @@ -362,6 +403,24 @@ func TestParseTimeAtOutsideLimits(t *testing.T) { f("2262-04-11T11:47:17-12:00") } +func TestParseTimeAtOutsideLimits_Nanos(t *testing.T) { + now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + + f := func(s string) { + t.Helper() + got, err := ParseTimeAt(s, now.UnixNano()) + if err == nil { + t.Fatalf("expected error but got %d", got) + } + if !strings.Contains(err.Error(), "cannot parse numeric timestamp") { + t.Fatalf("expected error: %v", err) + } + } + + // max unix nano + f(fmt.Sprintf("%d", uint64(math.MaxInt64+1))) +} + func TestParseTimeMsecFailure(t *testing.T) { f := func(s string) { t.Helper()
← Back to Alerts View on GitHub →