86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|