fix(v2): 限流按 socket 对端 IP(SetTrustedProxies(nil),伪造 XFF 失效)+ 桶 map 硬上限防 OOM
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package middleware_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -67,6 +68,66 @@ func TestIPRateLimiterPerIPIsolated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// jiu 反馈波安全 review:gin 默认 trustedProxies=0.0.0.0/0,不显式 SetTrustedProxies(nil)
|
||||
// 的话 c.ClientIP() 会信任客户端可控的 X-Forwarded-For——攻击者每次伪造不同 XFF 即可
|
||||
// 让限流器把每次请求都当"新 IP"的新桶,burst 永不触发(绕过限流)。
|
||||
// 这条测试用同一个 RemoteAddr(模拟同一 socket 源)、每次带不同的伪造 XFF 头发请求:
|
||||
// - 修复前(未调用 SetTrustedProxies(nil),沿用 gin 默认信任所有代理):ClientIP() 读
|
||||
// 伪造的 XFF,每次都是"新 IP"→ 新桶 → 全部放行,本测试会失败(RED)。
|
||||
// - 修复后(main.go 起 engine 时调用 r.SetTrustedProxies(nil)):ClientIP() 退回
|
||||
// RemoteIP()(socket 对端地址,请求头伪造不了)→ 同一来源共用一个桶 → burst 耗尽后
|
||||
// 触发 429,本测试通过(GREEN)。
|
||||
func TestIPRateLimiterSpoofedXFFCannotBypass(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rl := middleware.NewIPRateLimiter(3)
|
||||
r := gin.New()
|
||||
if err := r.SetTrustedProxies(nil); err != nil {
|
||||
t.Fatalf("SetTrustedProxies(nil) 失败: %v", err)
|
||||
}
|
||||
r.GET("/x", rl.Gin(), func(c *gin.Context) { c.Status(http.StatusOK) })
|
||||
|
||||
const sameSocketSource = "198.51.100.7:54321"
|
||||
var codes []int
|
||||
for i := 0; i < 4; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
req.RemoteAddr = sameSocketSource
|
||||
// 每次伪造一个不同的、格式合法的公网 IP——修复前的攻击手法。
|
||||
req.Header.Set("X-Forwarded-For", fmt.Sprintf("203.0.113.%d", i+1))
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
codes = append(codes, w.Code)
|
||||
}
|
||||
if codes[0] != http.StatusOK || codes[1] != http.StatusOK || codes[2] != http.StatusOK {
|
||||
t.Fatalf("同一 socket 源前 3 次(=burst)应放行, got codes=%v", codes)
|
||||
}
|
||||
if codes[3] != http.StatusTooManyRequests {
|
||||
t.Fatalf("同一 socket 源第 4 次应共桶触发 429(伪造 XFF 不能绕过限流), got codes=%v", codes)
|
||||
}
|
||||
}
|
||||
|
||||
// 不同 socket 源(不同 RemoteAddr)哪怕带相同的伪造 XFF,也应各自独立计桶——
|
||||
// 佐证限流键确实取自 c.ClientIP()(修复后=RemoteIP()),不是请求头。
|
||||
func TestIPRateLimiterDifferentSocketSourceIsolatedEvenWithSameXFF(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rl := middleware.NewIPRateLimiter(1)
|
||||
r := gin.New()
|
||||
if err := r.SetTrustedProxies(nil); err != nil {
|
||||
t.Fatalf("SetTrustedProxies(nil) 失败: %v", err)
|
||||
}
|
||||
r.GET("/x", rl.Gin(), func(c *gin.Context) { c.Status(http.StatusOK) })
|
||||
|
||||
for _, addr := range []string{"198.51.100.10:1111", "198.51.100.11:2222"} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
req.RemoteAddr = addr
|
||||
req.Header.Set("X-Forwarded-For", "203.0.113.99") // 两次都带同一个伪造 XFF
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("socket 源 %s 首次应 200(独立桶,不受相同 XFF 影响), got %d", addr, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// perMinute<=0 兜底成默认 30/min(config.RateLimitConfig 零值语义:"零值=默认开")。
|
||||
func TestNewIPRateLimiterZeroDefaultsTo30(t *testing.T) {
|
||||
rl := middleware.NewIPRateLimiter(0)
|
||||
|
||||
Reference in New Issue
Block a user