diff --git a/apps/api/internal/httpapi/callback_http.go b/apps/api/internal/httpapi/callback_http.go index 4bb8ef6b..2c072499 100644 --- a/apps/api/internal/httpapi/callback_http.go +++ b/apps/api/internal/httpapi/callback_http.go @@ -17,6 +17,9 @@ const callbackTimeout = 3 * time.Second var blockedCallbackPrefixes = []netip.Prefix{ netip.MustParsePrefix("0.0.0.0/8"), netip.MustParsePrefix("100.64.0.0/10"), + // Azure reserves this public-looking virtual IP for platform services + // inside every virtual network, including health and DHCP endpoints. + netip.MustParsePrefix("168.63.129.16/32"), netip.MustParsePrefix("192.0.0.0/24"), netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.31.196.0/24"), diff --git a/apps/api/internal/httpapi/integration_security_test.go b/apps/api/internal/httpapi/integration_security_test.go index a51f3250..7b6f84d9 100644 --- a/apps/api/internal/httpapi/integration_security_test.go +++ b/apps/api/internal/httpapi/integration_security_test.go @@ -159,6 +159,7 @@ func TestCallbackAddressPolicy(t *testing.T) { "127.0.0.1": false, "10.0.0.1": false, "169.254.169.254": false, + "168.63.129.16": false, "100.64.0.1": false, "198.18.0.1": false, "192.0.2.1": false, @@ -186,6 +187,27 @@ func TestCallbackAddressPolicy(t *testing.T) { } } +func TestCallbackDialerRejectsAzurePlatformVIPBeforeDial(t *testing.T) { + t.Parallel() + var dialed atomic.Bool + dialer := &callbackDialer{ + lookupNetIP: func(context.Context, string, string) ([]netip.Addr, error) { + t.Fatal("literal callback address unexpectedly triggered DNS resolution") + return nil, nil + }, + dialContext: func(context.Context, string, string) (net.Conn, error) { + dialed.Store(true) + return nil, errors.New("unexpected dial") + }, + } + if _, err := dialer.DialContext(context.Background(), "tcp", "168.63.129.16:80"); err == nil { + t.Fatal("expected Azure platform virtual IP to be rejected") + } + if dialed.Load() { + t.Fatal("callback dialer connected to the Azure platform virtual IP") + } +} + func TestCallbackDialerRejectsMixedDNSAnswersBeforeDial(t *testing.T) { t.Parallel() var dialed atomic.Bool @@ -271,6 +293,37 @@ func TestCallbackDeliveryBlocksLoopbackForBothCallbackTypes(t *testing.T) { } } +func TestCallbackDeliveryBlocksAzurePlatformVIPForBothCallbackTypes(t *testing.T) { + t.Parallel() + var dialed atomic.Bool + policyDialer := &callbackDialer{ + lookupNetIP: net.DefaultResolver.LookupNetIP, + dialContext: func(context.Context, string, string) (net.Conn, error) { + dialed.Store(true) + return nil, errors.New("unexpected dial") + }, + } + server := &Server{callbackClient: &http.Client{ + Transport: &http.Transport{DialContext: policyDialer.DialContext}, + Timeout: callbackTimeout, + }} + if _, _, err := server.postSlashCallback(context.Background(), store.SlashCommand{ + CallbackURL: "http://168.63.129.16/slash", + SigningSecret: "slash-secret", + }, []byte(`{"command":"/probe"}`)); err == nil { + t.Fatal("slash callback accepted the Azure platform virtual IP") + } + if _, _, err := server.postEventCallback(context.Background(), store.EventSubscription{ + CallbackURL: "http://168.63.129.16/event", + SigningSecret: "event-secret", + }, store.Event{ID: "evt_probe"}, []byte(`{"event":"probe"}`)); err == nil { + t.Fatal("event callback accepted the Azure platform virtual IP") + } + if dialed.Load() { + t.Fatal("callback delivery connected to the Azure platform virtual IP") + } +} + func expectSlashStatusAsUser(t *testing.T, userID, endpoint string, status int) { t.Helper() body := url.Values{"command": {"/deploy"}, "text": {"prod"}}.Encode()