Files
goclaw/internal/channels/ratelimit.go
T
viettranx 73b46c3634 feat(security): apply upstream TS OpenClaw security and core engine fixes
Port 8 fixes from upstream TypeScript OpenClaw (4 CRITICAL + 4 HIGH):

CRITICAL:
1. Tool call name trimming — add strings.TrimSpace() to all provider
   response parsers (Anthropic 2 locations, OpenAI 3 locations) to
   prevent registry lookup failures from LLM whitespace-padded names
2. Shell env injection deny patterns — block GIT_EXTERNAL_DIFF,
   GIT_DIFF_OPTS, BASH_ENV, and ENV=.*sh to prevent code execution
   via environment variable injection during git/shell operations
3. Broken symlink escape — recursive target resolution via
   resolveThroughExistingAncestors() to catch chained symlinks
   that escape workspace (e.g. link1→link2→/etc/passwd)
4. Mutable parent-symlink TOCTOU check — hasMutableSymlinkParent()
   detects symlinks in writable parent dirs that could be rebound
   between path validation and file operation

HIGH:
5. Model fallback thinking preservation — add ThinkingCapable interface
   to providers/types.go, implement on Anthropic/OpenAI/DashScope,
   check before injecting thinking_level in agent loop, warn on
   fallback when thinking is configured
6. Cron session-key double-prefix guard — prevent agent:X:cron:agent:X
   duplication in BuildCronSessionKey()
7. Webhook rate limiter — bounded WebhookRateLimiter (4096 keys max,
   60s window, 30 hits/window) to prevent memory exhaustion DoS
8. DM policy allowlist validation — warn at startup when
   dmPolicy=allowlist with empty allowFrom (silent message drop)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 16:52:37 +07:00

71 lines
1.7 KiB
Go

package channels
import (
"sync"
"time"
)
const (
// maxTrackedKeys caps the number of tracked rate-limit keys to prevent
// memory exhaustion from attackers rotating source IPs/keys.
maxTrackedKeys = 4096
// rateLimitWindow is the sliding window duration for rate counting.
rateLimitWindow = 60 * time.Second
// rateLimitMaxHits is the max requests per key within a window.
rateLimitMaxHits = 30
)
type rateLimitEntry struct {
windowStart time.Time
count int
}
// WebhookRateLimiter bounds the number of tracked rate-limit keys
// to prevent memory exhaustion from rotating source keys (DoS).
// Safe for concurrent use.
type WebhookRateLimiter struct {
mu sync.Mutex
entries map[string]*rateLimitEntry
}
// NewWebhookRateLimiter creates a bounded webhook rate limiter.
func NewWebhookRateLimiter() *WebhookRateLimiter {
return &WebhookRateLimiter{entries: make(map[string]*rateLimitEntry)}
}
// Allow returns true if the key is within rate limits.
// Automatically prunes stale entries and enforces a hard cap on tracked keys.
func (r *WebhookRateLimiter) Allow(key string) bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
// Prune stale entries when approaching the cap
if len(r.entries) >= maxTrackedKeys {
for k, e := range r.entries {
if now.Sub(e.windowStart) >= rateLimitWindow {
delete(r.entries, k)
}
}
// Hard eviction if still at cap (FIFO-ish via map iteration)
for len(r.entries) >= maxTrackedKeys {
for k := range r.entries {
delete(r.entries, k)
break
}
}
}
e, ok := r.entries[key]
if !ok || now.Sub(e.windowStart) >= rateLimitWindow {
r.entries[key] = &rateLimitEntry{windowStart: now, count: 1}
return true
}
e.count++
return e.count <= rateLimitMaxHits
}