Files
lijiaoqiao/supply-api/internal/middleware/ip_validation_test.go
Your Name e9523ea7a3 fix: 修复验证报告SEC-001和SEC-003安全问题
SEC-001: 移除硬编码"123456"测试码
- 修改DefaultSMSVerifier.Verify返回错误,强制要求配置真实SMS服务
- 添加ErrSMSServiceNotConfigured错误定义
- 更新相关测试使用mock SMS verifier

SEC-003: 添加IP欺骗防护
- AuthConfig添加TrustedProxies配置项
- getClientIP添加可信代理验证参数
- 仅在请求来自可信代理时信任X-Forwarded-For头
- 添加isTrustedProxy和containsCIDR辅助函数

架构重构:
- 创建internal/adapter包,包含存储适配器
- 创建internal/outbox包,包含OutboxProcessorRunner
- 创建internal/compensation包,包含补偿执行器
- main.go从891行减少到349行

TDD方法:
- 为每个修复编写测试用例
- 测试通过后再提交代码
2026-04-09 20:28:23 +08:00

251 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestGetClientIP_HeaderInjection
// TDD: 验证 getClientIP 函数能够处理恶意构造的 X-Forwarded-For 头
func TestGetClientIP_HeaderInjection(t *testing.T) {
// 可信代理配置
trustedProxies := []string{"192.168.0.0/16", "10.0.0.0/8"}
tests := []struct {
name string
headers map[string]string
remoteAddr string
trusted []string
expectedIP string
}{
{
name: "IP with port should be cleaned (trusted proxy)",
headers: map[string]string{"X-Forwarded-For": "203.0.113.1:8080"},
remoteAddr: "192.168.1.1:1234",
trusted: trustedProxies,
expectedIP: "203.0.113.1",
},
{
name: "Multiple IPs with spaces (trusted proxy)",
headers: map[string]string{"X-Forwarded-For": " 203.0.113.1 , 198.51.100.1 "},
remoteAddr: "192.168.1.1:1234",
trusted: trustedProxies,
expectedIP: "203.0.113.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = tt.remoteAddr
for k, v := range tt.headers {
req.Header.Set(k, v)
}
ip := getClientIP(req, tt.trusted...)
if ip != tt.expectedIP {
t.Errorf("expected IP %s, got %s", tt.expectedIP, ip)
}
})
}
}
// TestGetClientIP_PrivateIPInHeaders
// TDD: 验证来自不可信源的私有IP不应该被信任
func TestGetClientIP_PrivateIPInHeaders(t *testing.T) {
// 当请求来自外部remoteAddr是公网IPX-Forwarded-For中的私有IP可能是伪造的
tests := []struct {
name string
headers map[string]string
remoteAddr string
trustedCIDR []string
expectedIP string
}{
{
name: "Private IP in X-Forwarded-For from untrusted source",
headers: map[string]string{"X-Forwarded-For": "192.168.1.1"},
remoteAddr: "203.0.113.1:1234", // 公网IP
trustedCIDR: nil, // 未配置可信代理
expectedIP: "203.0.113.1", // 应该拒绝私有IP使用remoteAddr
},
{
name: "Loopback in X-Forwarded-For",
headers: map[string]string{"X-Forwarded-For": "127.0.0.1"},
remoteAddr: "203.0.113.1:1234",
trustedCIDR: nil,
expectedIP: "203.0.113.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = tt.remoteAddr
for k, v := range tt.headers {
req.Header.Set(k, v)
}
ip := getClientIP(req, tt.trustedCIDR...)
if ip != tt.expectedIP {
t.Errorf("expected IP %s, got %s", tt.expectedIP, ip)
}
})
}
}
// TestGetClientIP_TrustedProxy
// TDD: 配置了可信代理时,应该信任来自该代理的 X-Forwarded-For
func TestGetClientIP_TrustedProxy(t *testing.T) {
tests := []struct {
name string
headers map[string]string
remoteAddr string
trustedCIDR []string
expectedIP string
}{
{
name: "Trusted proxy returns first IP",
headers: map[string]string{"X-Forwarded-For": "203.0.113.1, 198.51.100.1"},
remoteAddr: "10.0.0.1:1234",
trustedCIDR: []string{"10.0.0.0/8"},
expectedIP: "203.0.113.1",
},
{
name: "Untrusted source ignores X-Forwarded-For",
headers: map[string]string{"X-Forwarded-For": "203.0.113.1"},
remoteAddr: "203.0.113.1:1234", // 公网IP
trustedCIDR: []string{"10.0.0.0/8"}, // 10.0.0.1不在可信范围内
expectedIP: "203.0.113.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = tt.remoteAddr
for k, v := range tt.headers {
req.Header.Set(k, v)
}
ip := getClientIP(req, tt.trustedCIDR...)
if ip != tt.expectedIP {
t.Errorf("expected IP %s, got %s", tt.expectedIP, ip)
}
})
}
}
// TestIsTrustedProxy_WithCIDR [SEC-003]
// 验证 isTrustedProxy 正确识别可信代理
func TestIsTrustedProxy_WithCIDR(t *testing.T) {
tests := []struct {
name string
remoteAddr string
trustedCIDR []string
expected bool
}{
{
name: "Private network is trusted",
remoteAddr: "10.0.0.5:1234",
trustedCIDR: []string{"10.0.0.0/8"},
expected: true,
},
{
name: "Public IP is not trusted",
remoteAddr: "203.0.113.1:1234",
trustedCIDR: []string{"10.0.0.0/8"},
expected: false,
},
{
name: "No trusted proxies configured",
remoteAddr: "10.0.0.5:1234",
trustedCIDR: nil,
expected: false,
},
{
name: "192.168 network is trusted",
remoteAddr: "192.168.1.1:1234",
trustedCIDR: []string{"192.168.0.0/16"},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isTrustedProxy(tt.remoteAddr, tt.trustedCIDR)
if result != tt.expected {
t.Errorf("isTrustedProxy(%s, %v) = %v, expected %v", tt.remoteAddr, tt.trustedCIDR, result, tt.expected)
}
})
}
}
// TestIsPublicIP [SEC-003]
// 验证 isPublicIP 正确识别公网IP
func TestIsPublicIP(t *testing.T) {
tests := []struct {
ip string
expected bool
}{
{"10.0.0.1", false},
{"192.168.1.1", false},
{"172.16.0.1", false},
{"127.0.0.1", false},
{"169.254.0.1", false},
{"0.0.0.0", false},
{"203.0.113.1", true},
{"198.51.100.1", true},
{"8.8.8.8", true},
}
for _, tt := range tests {
t.Run(tt.ip, func(t *testing.T) {
result := isPublicIP(tt.ip)
if result != tt.expected {
t.Errorf("isPublicIP(%s) = %v, expected %v", tt.ip, result, tt.expected)
}
})
}
}
// isPublicIP 检查是否为公网IP
func isPublicIP(ip string) bool {
// 检查是否为私有IP范围
if strings.HasPrefix(ip, "10.") {
return false
}
if strings.HasPrefix(ip, "192.168.") {
return false
}
if strings.HasPrefix(ip, "172.") {
// 172.16.0.0 - 172.31.255.255
parts := strings.Split(ip, ".")
if len(parts) >= 2 {
var secondOctet int
fmt.Sscanf(parts[1], "%d", &secondOctet)
if secondOctet >= 16 && secondOctet <= 31 {
return false
}
}
}
if strings.HasPrefix(ip, "127.") {
return false
}
if strings.HasPrefix(ip, "169.254.") {
return false
}
if strings.HasPrefix(ip, "0.") {
return false
}
return true
}