Files
coder/coderd/azureidentity/azureidentity_internal_test.go
Jakub Domeracki 57b11d405f fix(coderd): harden Azure identity certificate fetch (#25274)
Security improvements:
- Restrict cert fetches to a host+port allowlist (Microsoft and DigiCert
on 80/443).
- Route requests through a dedicated `http.Client` that resolves the
host once and dials the validated IP directly, preventing DNS rebinding.
- Reject loopback, private (RFC 1918 / IPv6 ULA), link-local, multicast,
unspecified, CGNAT, benchmarking, and IPv4-mapped IPv6 addresses.
- Cap the certificate response body at 1 MiB.
- Log the underlying error via slog and return a generic detail to the
caller to prevent information disclosure.
2026-05-13 12:51:44 +02:00

77 lines
2.3 KiB
Go

package azureidentity
import (
"context"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestIsPrivateIP(t *testing.T) {
t.Parallel()
cases := []struct {
name string
ip string
blocked bool
}{
{"loopback v4", "127.0.0.1", true},
{"loopback v6", "::1", true},
{"link local v4 (azure metadata)", "169.254.169.254", true},
{"link local v6", "fe80::1", true},
{"rfc1918 10/8", "10.0.0.1", true},
{"rfc1918 172.16/12", "172.16.0.1", true},
{"rfc1918 192.168/16", "192.168.0.1", true},
{"ipv6 ula", "fc00::1", true},
{"unspecified v4", "0.0.0.0", true},
{"unspecified v6", "::", true},
{"this-network 0.0.0.0/8", "0.1.2.3", true},
{"cgnat 100.64/10", "100.64.0.1", true},
{"benchmarking 198.18/15", "198.18.0.1", true},
{"multicast v4", "224.0.0.1", true},
{"ipv6 nat64 well-known", "64:ff9b:1::1", true},
{"ipv6 discard-only", "100::1", true},
{"ipv6 benchmarking", "2001:2::1", true},
{"ipv6 documentation", "2001:db8::1", true},
// IPv4-mapped IPv6: must canonicalize to v4 before
// classification, otherwise an attacker could bypass
// the metadata block via ::ffff:169.254.169.254.
{"ipv4-mapped metadata", "::ffff:169.254.169.254", true},
{"ipv4-mapped rfc1918", "::ffff:10.0.0.1", true},
{"public v4", "8.8.8.8", false},
{"public v6", "2606:4700:4700::1111", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ip := net.ParseIP(tc.ip)
require.NotNil(t, ip, "parse %q", tc.ip)
require.Equal(t, tc.blocked, isPrivateIP(ip))
})
}
}
// TestCertFetchClientRejectsLoopback proves the dialer refuses
// to connect even when the URL itself would have passed an
// allowlist (httptest.Server always binds to 127.0.0.1, so a
// successful fetch here would mean the SSRF guard had failed).
func TestCertFetchClientRejectsLoopback(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("should never be reached"))
}))
t.Cleanup(srv.Close)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil)
require.NoError(t, err)
resp, err := certFetchClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
require.Error(t, err)
require.Contains(t, err.Error(), "private IP")
}