Compare commits

...
2 Commits
Author SHA1 Message Date
lukaszraczylo eaf7c4ddca fix(worker): key rate limiter on peer IP, not forged headers
The rate limiter trusted X-Real-IP, so a client could dodge its limit by
sending a new header value on each request. It must key on the connection
peer address. The key also strips the port, so one client shares one bucket.
2026-10-03 12:02:17 +01:00
github-actions[bot] 2d1f52cdf4 chore: update marketplace for v0.21.81 2026-10-03 04:05:45 +00:00
4 changed files with 49 additions and 14 deletions
+4 -4
View File
@@ -2,6 +2,7 @@
package worker
import (
"net"
"net/http"
"sync"
"time"
@@ -206,14 +207,13 @@ func (pcrl *PerClientRateLimiter) Stats() map[string]any {
}
// PerClientRateLimitMiddleware creates middleware that applies per-client rate limiting.
// Uses X-Forwarded-For or RemoteAddr to identify clients.
// Identifies clients by the peer IP of the connection; forwarding headers are spoofable and ignored.
func PerClientRateLimitMiddleware(limiter *PerClientRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get client identifier (prefer X-Real-IP from RealIP middleware)
clientKey := r.RemoteAddr
if xff := r.Header.Get("X-Real-IP"); xff != "" {
clientKey = xff
if host, _, err := net.SplitHostPort(clientKey); err == nil {
clientKey = host
}
if !limiter.Allow(clientKey) {
+35
View File
@@ -0,0 +1,35 @@
package worker
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestPerClientRateLimitMiddleware_KeysOnPeerIP(t *testing.T) {
h := PerClientRateLimitMiddleware(NewPerClientRateLimiter(0.001, 1))(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
do := func(remote, realIP string) int {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = remote
if realIP != "" {
req.Header.Set("X-Real-IP", realIP)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Code
}
if got := do("10.0.0.1:1111", "1.1.1.1"); got != http.StatusOK {
t.Fatalf("first request: got %d", got)
}
// Same peer, new port and forged header: must share the exhausted bucket.
if got := do("10.0.0.1:2222", "2.2.2.2"); got != http.StatusTooManyRequests {
t.Fatalf("spoofed header/new port bypassed limit: got %d", got)
}
// Different peer is unaffected.
if got := do("10.0.0.2:1111", ""); got != http.StatusOK {
t.Fatalf("other peer: got %d", got)
}
}
+1 -1
View File
@@ -1226,7 +1226,7 @@ func (s *Service) setupMiddleware() {
// Add gzip compression for responses >1KB (reduces bandwidth ~70% for JSON)
s.router.Use(middleware.Compress(5)) // Level 5 = good balance of speed vs compression
// Apply per-client rate limiting (after RealIP so we get the real client IP)
// Apply per-client rate limiting (keyed on the socket peer address; forwarding headers are untrusted)
if s.rateLimiter != nil {
s.router.Use(PerClientRateLimitMiddleware(s.rateLimiter))
}
+9 -9
View File
@@ -13,7 +13,7 @@
{
"name": "claude-mnemonic",
"description": "Persistent memory system for Claude Code - Go implementation with SQLite and ChromaDB vector search",
"version": "0.21.77",
"version": "0.21.81",
"author": {
"name": "lukaszraczylo",
"url": "https://github.com/lukaszraczylo"
@@ -23,25 +23,25 @@
"license": "MIT",
"homepage": "https://github.com/lukaszraczylo/claude-mnemonic",
"releases": {
"latest": "0.21.77",
"latest": "0.21.81",
"versions": {
"0.21.77": {
"0.21.81": {
"releaseDate": "2026-10-03",
"downloads": {
"darwin-arm64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.21.77/claude-mnemonic_0.21.77_darwin_arm64.tar.gz",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.21.81/claude-mnemonic_0.21.81_darwin_arm64.tar.gz",
"format": "tar.gz",
"sha256": "7bf9c814f4c2d1e9a727c24b0b487bd207dbdc45ed0828aef23e53017f3019d7"
"sha256": "8da5803dc0acd44b6e43b4c8d7ff2667811dd8ea5f7bf40643c91829e3e2c37c"
},
"linux-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.21.77/claude-mnemonic_0.21.77_linux_amd64.tar.gz",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.21.81/claude-mnemonic_0.21.81_linux_amd64.tar.gz",
"format": "tar.gz",
"sha256": "cf5998292c72662b85a25df539a340b8c24e9caa2488fed9fd817d758124eac2"
"sha256": "b1a1e7a6317280ef2bce92e349ffdbbf0c9df5cf92f4bce0378c34194b871c01"
},
"windows-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.21.77/claude-mnemonic_0.21.77_windows_amd64.zip",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.21.81/claude-mnemonic_0.21.81_windows_amd64.zip",
"format": "zip",
"sha256": "762ca4da50188763928802f148ec918e9f02d7678400fe17e46f84d82f5ed57b"
"sha256": "869b99958371451a8efe91cc91a9f0eb654a355c0b77a769ab9916e7908c11b1"
}
}
}