package api // Issue #85: the rate limit key must use the rightmost X-Forwarded-For entry // (appended by the trusted Traefik proxy), never the raw/leftmost header // value a client can forge. A spoofed FIRST XFF entry must not bypass the // limit or rotate buckets. import ( "bytes" "net/http/httptest" "testing" ) func TestClientIPTakesRightmostXFF(t *testing.T) { r := httptest.NewRequest("POST", "/", nil) r.RemoteAddr = "10.42.0.7:51000" // trusted Traefik pod r.Header.Set("X-Forwarded-For", "1.2.3.4, 1.2.3.5, 203.0.113.9") if got := clientIP(r); got != "203.0.113.9" { t.Fatalf("clientIP = %q, want rightmost 203.0.113.9", got) } } func TestClientIPXRealIPFallback(t *testing.T) { r := httptest.NewRequest("POST", "/", nil) r.RemoteAddr = "10.42.0.7:51000" r.Header.Set("X-Real-Ip", "203.0.113.10") if got := clientIP(r); got != "203.0.113.10" { t.Fatalf("clientIP = %q, want 203.0.113.10", got) } } func TestClientIPDirectFallback(t *testing.T) { r := httptest.NewRequest("POST", "/", nil) r.RemoteAddr = "198.51.100.5:51000" if got := clientIP(r); got != "198.51.100.5" { t.Fatalf("clientIP = %q, want 198.51.100.5", got) } } // TestRateLimitSpoofedFirstXFFDoesNotBypass: an attacker rotating a fake // leftmost XFF entry stays limited on their real (rightmost) IP. func TestRateLimitSpoofedFirstXFFDoesNotBypass(t *testing.T) { srv := newTestServer(t) h := srv.routes() for i := 0; i < 5; i++ { req := httptest.NewRequest("POST", "/api/pastes", bytes.NewReader([]byte(`{"content":"hi"}`))) req.RemoteAddr = "10.42.0.7:51000" // each request spoofs a DIFFERENT leftmost entry req.Header.Set("X-Forwarded-For", spoofN(i)+", 203.0.113.9") rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != 201 { t.Fatalf("req %d: want 201, got %d", i, rr.Code) } } // 6th request, still the same real IP, new spoofed prefix: must 429 req := httptest.NewRequest("POST", "/api/pastes", bytes.NewReader([]byte(`{"content":"hi"}`))) req.RemoteAddr = "10.42.0.7:51000" req.Header.Set("X-Forwarded-For", "9.9.9.9, 203.0.113.9") rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != 429 { t.Fatalf("spoofed 6th req: want 429, got %d", rr.Code) } } func spoofN(i int) string { return "1.2.3." + string(rune('0'+i)) } // Distinct real IPs must still get distinct buckets (no over-limiting). func TestRateLimitDistinctRightmostIPsIndependent(t *testing.T) { srv := newTestServer(t) h := srv.routes() for _, ip := range []string{"203.0.113.20", "203.0.113.21"} { req := httptest.NewRequest("POST", "/api/pastes", bytes.NewReader([]byte(`{"content":"hi"}`))) req.RemoteAddr = "10.42.0.7:51000" req.Header.Set("X-Forwarded-For", "6.6.6.6, "+ip) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != 201 { t.Fatalf("ip %s: want 201, got %d", ip, rr.Code) } } }