Validate resolved dial IPs in the channel monitor HTTP transport so DNS rebinding cannot bypass SSRF checks.
136 lines
3.7 KiB
Go
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)
|
|
}
|