From 98e56003418223162290ff63f228deeb228ea5d6 Mon Sep 17 00:00:00 2001 From: alhudz Date: Wed, 10 Jun 2026 11:01:59 +0530 Subject: [PATCH 1/2] reject non-rfc3339 timestamp strings in timestamp() conversion --- common/types/string.go | 11 +++++++---- common/types/string_test.go | 29 +++++++++++++++++++++++++++++ common/types/timestamp.go | 10 ++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/common/types/string.go b/common/types/string.go index 5f5a43358..5bdb294b8 100644 --- a/common/types/string.go +++ b/common/types/string.go @@ -122,11 +122,14 @@ func (s String) ConvertToType(typeVal ref.Type) ref.Val { return durationOf(d) } case TimestampType: - if t, err := time.Parse(time.RFC3339, s.Value().(string)); err == nil { - if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { - return celErrTimestampOverflow + str := s.Value().(string) + if strictRFC3339Pattern.MatchString(str) { + if t, err := time.Parse(time.RFC3339, str); err == nil { + if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { + return celErrTimestampOverflow + } + return timestampOf(t) } - return timestampOf(t) } case StringType: return s diff --git a/common/types/string_test.go b/common/types/string_test.go index 158f2bb74..af2a9a067 100644 --- a/common/types/string_test.go +++ b/common/types/string_test.go @@ -163,6 +163,35 @@ func TestStringConvertToType(t *testing.T) { } } +func TestStringConvertToTimestampStrict(t *testing.T) { + valid := []string{ + "2025-01-17T01:00:00.001Z", + "2025-01-01T12:34:56Z", + "2025-01-01T12:34:56.123456789Z", + "2025-01-01T12:34:56+05:30", + "2025-01-01T12:34:56-08:00", + "2025-01-01T12:34:56+14:00", + } + for _, s := range valid { + if IsError(String(s).ConvertToType(TimestampType)) { + t.Errorf("String(%q).ConvertToType(TimestampType) errored, wanted a timestamp", s) + } + } + // RFC 3339 violations that time.Parse accepts loosely. + invalid := []string{ + "2025-01-17T01:00:00,001Z", // ',' fractional separator + "2025-01-17T1:00:00Z", // single-digit hour + "2025-01-17T01:5:00Z", // single-digit minute + "2025-01-18T01:01:01.001+24:01", // offset hour out of range + "2025-01-17T01:01:01.001+00:60", // offset minute out of range + } + for _, s := range invalid { + if !IsError(String(s).ConvertToType(TimestampType)) { + t.Errorf("String(%q).ConvertToType(TimestampType) succeeded, wanted an error", s) + } + } +} + func TestStringEqual(t *testing.T) { if !String("hello").Equal(String("hello")).(Bool) { t.Error("Two equivalent strings were not equal") diff --git a/common/types/timestamp.go b/common/types/timestamp.go index 060caf6bb..9c6dfa1d5 100644 --- a/common/types/timestamp.go +++ b/common/types/timestamp.go @@ -17,6 +17,7 @@ package types import ( "fmt" "reflect" + "regexp" "strconv" "strings" "time" @@ -52,6 +53,15 @@ const ( maxUnixTime int64 = 253402300799 ) +// strictRFC3339Pattern gates the strings accepted by the `timestamp()` overload. +// time.Parse accepts inputs that RFC 3339 forbids: a ',' fractional-second +// separator, single-digit time fields, and numeric offsets whose hours exceed +// 23 or minutes exceed 59. Those slip past unnoticed and shift the parsed +// instant, so they are rejected before time.Parse runs. The remaining calendar +// validation (month, day, leap year) is left to time.Parse. +var strictRFC3339Pattern = regexp.MustCompile( + `^\d{4}-\d{2}-\d{2}[Tt]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$`) + // Add implements traits.Adder.Add. func (t Timestamp) Add(other ref.Val) ref.Val { switch other.Type() { From a6f83c32b6f685379b268d94c60b1a4f09a8181c Mon Sep 17 00:00:00 2001 From: alhudz Date: Fri, 12 Jun 2026 09:57:01 +0530 Subject: [PATCH 2/2] return early with a specific error when the timestamp format is invalid --- common/types/string.go | 13 +++++++------ common/types/string_test.go | 9 ++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/common/types/string.go b/common/types/string.go index 5bdb294b8..c4847f7ad 100644 --- a/common/types/string.go +++ b/common/types/string.go @@ -123,13 +123,14 @@ func (s String) ConvertToType(typeVal ref.Type) ref.Val { } case TimestampType: str := s.Value().(string) - if strictRFC3339Pattern.MatchString(str) { - if t, err := time.Parse(time.RFC3339, str); err == nil { - if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { - return celErrTimestampOverflow - } - return timestampOf(t) + if !strictRFC3339Pattern.MatchString(str) { + return NewErr("invalid RFC 3339 timestamp %q", str) + } + if t, err := time.Parse(time.RFC3339, str); err == nil { + if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { + return celErrTimestampOverflow } + return timestampOf(t) } case StringType: return s diff --git a/common/types/string_test.go b/common/types/string_test.go index af2a9a067..664e39904 100644 --- a/common/types/string_test.go +++ b/common/types/string_test.go @@ -15,6 +15,7 @@ package types import ( + "fmt" "reflect" "testing" "time" @@ -186,8 +187,14 @@ func TestStringConvertToTimestampStrict(t *testing.T) { "2025-01-17T01:01:01.001+00:60", // offset minute out of range } for _, s := range invalid { - if !IsError(String(s).ConvertToType(TimestampType)) { + out := String(s).ConvertToType(TimestampType) + if !IsError(out) { t.Errorf("String(%q).ConvertToType(TimestampType) succeeded, wanted an error", s) + continue + } + want := fmt.Sprintf("invalid RFC 3339 timestamp %q", s) + if got := out.(*Err).String(); got != want { + t.Errorf("String(%q).ConvertToType(TimestampType) errored with %q, wanted %q", s, got, want) } } }