test: security regression — IDOR payment-check scoping, CORS preflight 403, rate limiter pruning, services error leakage
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -950,3 +950,80 @@ func TestProgressiveRateLimit_RejectsOnlyTopTier(t *testing.T) {
|
||||
t.Errorf("the 10s tier must reject immediately, not sleep (took %s)", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ProgressiveRateLimiter pruning: timestamps pruned before counting
|
||||
// ============================================================
|
||||
|
||||
// TestProgressiveRateLimiter_TimestampsBoundedAfterManyCalls verifies that
|
||||
// calling Check() many times does not cause unbounded growth of the timestamps
|
||||
// slice. The pruning step in Check() should keep the slice bounded to at most
|
||||
// the number of timestamps that fit within the 60-second window.
|
||||
func TestProgressiveRateLimiter_TimestampsBoundedAfterManyCalls(t *testing.T) {
|
||||
prl := NewProgressiveRateLimiter()
|
||||
ip := "198.51.100.77"
|
||||
|
||||
// Make 100 rapid calls — each appends a timestamp, then the pruning step
|
||||
// removes anything older than 60s. Since all 100 calls happen within much
|
||||
// less than 60s, the slice should contain at most 100 entries after pruning.
|
||||
for i := 0; i < 100; i++ {
|
||||
_ = prl.Check(ip)
|
||||
}
|
||||
|
||||
prl.mu.RLock()
|
||||
state, exists := prl.requests[ip]
|
||||
prl.mu.RUnlock()
|
||||
if !exists {
|
||||
t.Fatal("expected ip state to exist after 100 calls")
|
||||
}
|
||||
if len(state.timestamps) > 100 {
|
||||
t.Errorf("timestamps unbounded after 100 calls: got %d entries", len(state.timestamps))
|
||||
}
|
||||
|
||||
// The slice should not have more than 120 entries (the sustained limit)
|
||||
// after rapid calling — the pruning in Check() keeps it bounded.
|
||||
maxExpected := 120
|
||||
if len(state.timestamps) > maxExpected {
|
||||
t.Errorf("expected at most %d timestamps after pruning, got %d", maxExpected, len(state.timestamps))
|
||||
}
|
||||
}
|
||||
|
||||
// TestProgressiveRateLimiter_OldTimestampsPrunedOnCheck verifies that
|
||||
// timestamps older than 60 seconds are pruned when Check() runs. This is the
|
||||
// core of the fix: the pruning happens BEFORE counting, so stale timestamps
|
||||
// from burst traffic cannot inflate the sustained count.
|
||||
func TestProgressiveRateLimiter_OldTimestampsPrunedOnCheck(t *testing.T) {
|
||||
prl := NewProgressiveRateLimiter()
|
||||
ip := "198.51.100.78"
|
||||
|
||||
// Seed timestamps: 10 recent (within 5s) + 100 old (65-120s ago).
|
||||
// After pruning, only the 10 recent timestamps should remain.
|
||||
now := clock.Now()
|
||||
ts := make([]time.Time, 0, 110)
|
||||
for i := 0; i < 10; i++ {
|
||||
ts = append(ts, now.Add(-time.Second))
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
ts = append(ts, now.Add(-time.Duration(65+i)*time.Second))
|
||||
}
|
||||
prl.mu.Lock()
|
||||
prl.requests[ip] = &ipProgressiveState{timestamps: ts}
|
||||
prl.mu.Unlock()
|
||||
|
||||
// Check() should prune timestamps before counting.
|
||||
_ = prl.Check(ip)
|
||||
|
||||
prl.mu.RLock()
|
||||
state := prl.requests[ip]
|
||||
count := len(state.timestamps)
|
||||
prl.mu.RUnlock()
|
||||
|
||||
// After pruning, we should have at most 11 entries (10 recent + the one
|
||||
// just appended by Check()). All 100 old timestamps should be gone.
|
||||
if count > 20 {
|
||||
t.Errorf("expected old timestamps to be pruned — got %d entries, expected <= 11", count)
|
||||
}
|
||||
if count < 5 {
|
||||
t.Errorf("expected recent timestamps to survive pruning — got only %d entries", count)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user