package web import ( "net/http" "net/http/httptest" "testing" ) // S6 回归:X-Forwarded-For 只有**最后一段**可信。 // // 反代(nginx 的 `$proxy_add_x_forwarded_for`)是把客户端发来的值原样接在前面、 // 再把真实对端补在后面。所以取第一段等于「谁想冒充谁自己填」——访客随手改个请求头 // 就能换一个投票身份。取最后一段才是反代自己写、客户端改不动的那一个。 func TestClientIPTrustsOnlyProxyWrittenSegment(t *testing.T) { cases := []struct { name string remote string xff []string want string }{ {"没有 XFF 时用 RemoteAddr", "203.0.113.7:51234", nil, "203.0.113.7"}, {"RemoteAddr 不带端口", "203.0.113.7", nil, "203.0.113.7"}, {"单段 XFF(反代覆盖式写入)", "10.0.0.1:80", []string{"203.0.113.7"}, "203.0.113.7"}, {"多段 XFF 取最后一段", "10.0.0.1:80", []string{"198.51.100.9, 203.0.113.7"}, "203.0.113.7"}, {"最后一段两边有空格", "10.0.0.1:80", []string{"198.51.100.9 , 203.0.113.7 "}, "203.0.113.7"}, {"多行 XFF 拼接后取最后", "10.0.0.1:80", []string{"198.51.100.9", "203.0.113.7"}, "203.0.113.7"}, {"最后一段带端口", "10.0.0.1:80", []string{"198.51.100.9, 203.0.113.7:51234"}, "203.0.113.7"}, {"IPv6", "10.0.0.1:80", []string{"198.51.100.9, 2001:db8::1"}, "2001:db8::1"}, {"IPv4-mapped IPv6 归一", "10.0.0.1:80", []string{"::ffff:203.0.113.7"}, "203.0.113.7"}, // 最后一段不是 IP ⇒ 整个头作废,别把垃圾当身份 {"最后一段是垃圾", "203.0.113.7:51234", []string{"198.51.100.9, not-an-ip"}, "203.0.113.7"}, {"XFF 只有垃圾", "203.0.113.7:51234", []string{"whatever"}, "203.0.113.7"}, {"XFF 为空串", "203.0.113.7:51234", []string{""}, "203.0.113.7"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { r := httptest.NewRequest("POST", "/api/vote", nil) r.RemoteAddr = c.remote for _, v := range c.xff { r.Header.Add("X-Forwarded-For", v) } if got := clientIP(r); got != c.want { t.Errorf("clientIP = %q,期望 %q", got, c.want) } }) } } // 真正要守的性质:只改伪造的前缀,不该改变访客身份。 // 这条比上面任何单点断言都硬——它直接对应「换个请求头就换一票」这个攻击。 func TestClientIPIgnoresForgedPrefix(t *testing.T) { mk := func(forged string) *http.Request { r := httptest.NewRequest("POST", "/api/vote", nil) r.RemoteAddr = "10.0.0.1:80" r.Header.Set("X-Forwarded-For", forged+", 203.0.113.7") return r } base := clientIP(mk("")) for _, forged := range []string{"1.2.3.4", "9.9.9.9", "::1", "198.51.100.9"} { got := clientIP(mk(forged)) if got != base { t.Errorf("伪造前缀 %q 让身份变成了 %q(基线 %q)——换个头就能换一票", forged, got, base) } } }