fix(tasks): replace O(n) loop in repeating-task handler with arithmetic
addRepeatIntervalToTime used to advance t by whole intervals via an unbounded loop. A repeating task with an ancient due_date and a one-second interval required billions of iterations per task update, turning completion of such a task into a trivial denial-of-service (GHSA-r4fg-73rc-hhh7). Compute the number of intervals directly, with guards for zero/negative durations, saturated time.Sub, and int64 overflow. Covered by TestAddRepeatIntervalToTime, including the 1900-01-01 PoC case.
This commit is contained in:
parent
879462d717
commit
9dc3d7eb4f
|
|
@ -1508,15 +1508,36 @@ func addOneMonthToDate(d time.Time) time.Time {
|
|||
return time.Date(d.Year(), d.Month()+1, d.Day(), d.Hour(), d.Minute(), d.Second(), d.Nanosecond(), config.GetTimeZone())
|
||||
}
|
||||
|
||||
// addRepeatIntervalToTime advances t by whole multiples of duration until
|
||||
// it is strictly after now. The previous O(n) loop made a one-second
|
||||
// interval with an ancient due_date trivial DoS (GHSA-r4fg-73rc-hhh7);
|
||||
// this computes the answer in constant time.
|
||||
func addRepeatIntervalToTime(now, t time.Time, duration time.Duration) time.Time {
|
||||
for {
|
||||
t = t.Add(duration)
|
||||
if t.After(now) {
|
||||
break
|
||||
}
|
||||
if duration <= 0 {
|
||||
return t
|
||||
}
|
||||
|
||||
return t
|
||||
// Preserve the original contract: always advance t by at least one
|
||||
// interval, even when t is already at or after now.
|
||||
if !t.Before(now) {
|
||||
return t.Add(duration)
|
||||
}
|
||||
|
||||
// time.Time.Sub saturates at math.MaxInt64 nanoseconds (~292 years).
|
||||
// Fall back to "one interval past now" for pathologically old t.
|
||||
diff := now.Sub(t)
|
||||
if diff == math.MaxInt64 {
|
||||
return now.Add(duration)
|
||||
}
|
||||
|
||||
intervals := int64(diff/duration) + 1
|
||||
|
||||
// Guard against int64 overflow when multiplying intervals by duration.
|
||||
if intervals > math.MaxInt64/int64(duration) {
|
||||
return now.Add(duration)
|
||||
}
|
||||
|
||||
return t.Add(time.Duration(intervals) * duration)
|
||||
}
|
||||
|
||||
func setTaskDatesDefault(oldTask, newTask *Task) {
|
||||
|
|
|
|||
|
|
@ -988,6 +988,90 @@ func TestUpdateDone(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAddRepeatIntervalToTime(t *testing.T) {
|
||||
now := time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC)
|
||||
day := 24 * time.Hour
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
now time.Time
|
||||
t time.Time
|
||||
duration time.Duration
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "one day interval, t one day before now",
|
||||
now: now,
|
||||
t: now.Add(-day),
|
||||
duration: day,
|
||||
want: now.Add(day),
|
||||
},
|
||||
{
|
||||
name: "one day interval, t exactly one week before now",
|
||||
now: now,
|
||||
t: now.Add(-7 * day),
|
||||
duration: day,
|
||||
want: now.Add(day),
|
||||
},
|
||||
{
|
||||
name: "t in the far past (PoC case) completes with sane result",
|
||||
now: now,
|
||||
t: time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
duration: time.Second,
|
||||
want: now.Add(time.Second),
|
||||
},
|
||||
{
|
||||
name: "zero t saturates and falls back",
|
||||
now: now,
|
||||
t: time.Time{},
|
||||
duration: time.Hour,
|
||||
want: now.Add(time.Hour),
|
||||
},
|
||||
{
|
||||
name: "t after now still advances by one interval",
|
||||
now: now,
|
||||
t: now.Add(time.Hour),
|
||||
duration: day,
|
||||
want: now.Add(time.Hour + day),
|
||||
},
|
||||
{
|
||||
name: "t equals now still advances",
|
||||
now: now,
|
||||
t: now,
|
||||
duration: day,
|
||||
want: now.Add(day),
|
||||
},
|
||||
{
|
||||
name: "zero duration returns t unchanged",
|
||||
now: now,
|
||||
t: now.Add(-day),
|
||||
duration: 0,
|
||||
want: now.Add(-day),
|
||||
},
|
||||
{
|
||||
name: "negative duration returns t unchanged",
|
||||
now: now,
|
||||
t: now.Add(-day),
|
||||
duration: -time.Hour,
|
||||
want: now.Add(-day),
|
||||
},
|
||||
{
|
||||
name: "tiny duration on ancient date does not overflow",
|
||||
now: now,
|
||||
t: time.Date(1800, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
duration: 1,
|
||||
want: now.Add(1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := addRepeatIntervalToTime(tc.now, tc.t, tc.duration)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTask_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue