mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-09 19:10:12 +00:00
73b46c3634
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>
71 lines
1.7 KiB
Go
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
|
|
}
|