new-api/service/channel_monitor_ssrf_test.go
zizi 0625c6304b fix: protect channel monitor dial targets
Validate resolved dial IPs in the channel monitor HTTP transport so DNS rebinding cannot bypass SSRF checks.
2026-05-20 15:41:06 +08:00

136 lines
3.7 KiB
Go

package service
import (
"context"
"errors"
"net"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestValidateChannelMonitorTargetURLRejectsInternalAndInvalidTargets(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
target string
errorContains string
}{
{
name: "reject loopback IPv4",
target: "http://127.0.0.1:3000/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject localhost resolved IP",
target: "http://localhost:3000/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject 10 private range",
target: "http://10.0.0.1/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject 172.16 private range",
target: "http://172.16.0.1/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject 172.31 private range",
target: "https://172.31.255.255/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject 192.168 private range",
target: "http://192.168.1.10/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject link-local IPv4",
target: "http://169.254.169.254/latest/meta-data",
errorContains: "private/internal IP not allowed",
},
{
name: "reject loopback IPv6",
target: "http://[::1]:3000/v1/chat/completions",
errorContains: "private/internal IP not allowed",
},
{
name: "reject unsupported scheme",
target: "file:///etc/passwd",
errorContains: "only http and https allowed",
},
{
name: "reject invalid URL",
target: "http://[::1",
errorContains: "invalid URL",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := ValidateChannelMonitorTargetURL(tc.target)
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
})
}
}
func TestValidateChannelMonitorTargetURLAllowsPublicHTTPSTarget(t *testing.T) {
t.Parallel()
err := ValidateChannelMonitorTargetURL("https://8.8.8.8/v1/chat/completions")
require.NoError(t, err)
}
func TestChannelMonitorProtectedDialContextRejectsResolvedInternalIP(t *testing.T) {
t.Parallel()
dialCalled := false
dialContext := channelMonitorProtectedDialContext(
func(context.Context, string, string) (net.Conn, error) {
dialCalled = true
return nil, errors.New("dial should not be called")
},
func(context.Context, string) ([]net.IPAddr, error) {
return []net.IPAddr{{IP: net.ParseIP("127.0.0.1")}}, nil
},
)
conn, err := dialContext(context.Background(), "tcp", "rebind.example:443")
require.Nil(t, conn)
require.Error(t, err)
require.Contains(t, err.Error(), "private/internal IP not allowed")
require.False(t, dialCalled)
}
func TestChannelMonitorProtectedDialContextDialsResolvedPublicIP(t *testing.T) {
t.Parallel()
var dialedAddress string
dialContext := channelMonitorProtectedDialContext(
func(_ context.Context, _ string, addr string) (net.Conn, error) {
dialedAddress = strings.TrimSpace(addr)
return nil, errors.New("stop after address capture")
},
func(context.Context, string) ([]net.IPAddr, error) {
return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}, nil
},
)
conn, err := dialContext(context.Background(), "tcp", "example.com:443")
require.Nil(t, conn)
require.Error(t, err)
require.Contains(t, err.Error(), "stop after address capture")
require.Equal(t, "93.184.216.34:443", dialedAddress)
}