package tool import ( "context" "net" "strings" "testing" ) // TestResolveAndCheck_AllowlistedPublic anchors the happy path: a // public domain in the allowlist resolves and returns its IP. Skips // when DNS isn't available so the suite still passes in offline CI. func TestResolveAndCheck_AllowlistedPublic(t *testing.T) { // Pre-flight: skip the test if the test environment has no DNS. if _, err := net.LookupHost("example.com"); err != nil { t.Skipf("no DNS in test environment: %v", err) } allow := AllowlistConfig{Domains: []string{"example.com"}} ip, err := ResolveAndCheck(context.Background(), "https://example.com/", allow) if err != nil { t.Fatalf("ResolveAndCheck failed: %v", err) } if ip == nil { t.Fatal("expected non-nil IP") } } // TestResolveAndCheck_NotAllowlisted ensures a domain outside the // allowlist is rejected before any DNS resolution. func TestResolveAndCheck_NotAllowlisted(t *testing.T) { allow := AllowlistConfig{Domains: []string{"example.com"}} _, err := ResolveAndCheck(context.Background(), "https://evil.test/", allow) if err == nil { t.Fatal("expected rejection for non-allowlisted host") } if !strings.Contains(err.Error(), "not in allowlist") { t.Errorf("expected allowlist error, got: %v", err) } } // TestResolveAndCheck_WildcardMatch confirms "*.example.com" matches // foo.example.com. func TestResolveAndCheck_WildcardMatch(t *testing.T) { if !matchesAllowlist("foo.example.com", []string{"*.example.com"}) { t.Error("expected *.example.com to match foo.example.com") } if !matchesAllowlist("a.b.example.com", []string{"*.example.com"}) { t.Error("expected *.example.com to match a.b.example.com") } } // TestResolveAndCheck_WildcardDoesNotMatchBareDomain documents the // design choice: "*.example.com" does NOT match "example.com" itself. // Admins who want both must list both entries. func TestResolveAndCheck_WildcardDoesNotMatchBareDomain(t *testing.T) { if matchesAllowlist("example.com", []string{"*.example.com"}) { t.Error("expected *.example.com NOT to match bare example.com") } } // TestResolveAndCheck_LocalhostRejected verifies that a hostname // resolving to 127.0.0.1 is rejected unless the admin explicitly // includes it in the allowlist. func TestResolveAndCheck_LocalhostRejected(t *testing.T) { if _, err := net.LookupHost("localhost"); err != nil { t.Skipf("no DNS in test environment: %v", err) } // "localhost" matches the allowlist by exact name match, but the // hostExplicitlyAllowed bypass kicks in only when the host is in // the allowlist as an exact entry. Here we use a DIFFERENT bare // allowlist entry so the host fails the allowlist match outright. allow := AllowlistConfig{Domains: []string{"example.com"}} _, err := ResolveAndCheck(context.Background(), "http://localhost/", allow) if err == nil { t.Fatal("expected rejection for localhost (not in allowlist)") } } // TestResolveAndCheck_LocalhostAllowedExplicit confirms the // admin-opt-in escape hatch: when the hostname is itself in the // allowlist as an exact entry, the private-IP check is bypassed. // This is what test code uses to drive httptest.NewServer URLs. func TestResolveAndCheck_LocalhostAllowedExplicit(t *testing.T) { // Use 127.0.0.1 directly so this test doesn't depend on DNS for // "localhost". allow := AllowlistConfig{Domains: []string{"127.0.0.1"}} ip, err := ResolveAndCheck(context.Background(), "http://127.0.0.1/", allow) if err != nil { t.Fatalf("expected 127.0.0.1 with explicit allowlist to succeed; got: %v", err) } if !ip.Equal(net.ParseIP("127.0.0.1")) { t.Errorf("expected ip=127.0.0.1, got %v", ip) } } // TestResolveAndCheck_FileSchemeRejected blocks file:// URLs. func TestResolveAndCheck_FileSchemeRejected(t *testing.T) { allow := AllowlistConfig{Domains: []string{"anything"}} _, err := ResolveAndCheck(context.Background(), "file:///etc/passwd", allow) if err == nil { t.Fatal("expected rejection for file:// scheme") } if !strings.Contains(err.Error(), "scheme") { t.Errorf("expected scheme error, got: %v", err) } } // TestResolveAndCheck_EmptyHostRejected blocks malformed URLs with // no host component. func TestResolveAndCheck_EmptyHostRejected(t *testing.T) { allow := AllowlistConfig{Domains: []string{"anything"}} _, err := ResolveAndCheck(context.Background(), "http:///nohost", allow) if err == nil { t.Fatal("expected rejection for empty host") } } // TestResolveAndCheck_PrivateIPLiteralRejected confirms that an IP // literal resolving to the private range is rejected even if the // allowlist matches by wildcard or other means. The private-IP gate // is the last line of defence. func TestResolveAndCheck_PrivateIPLiteralRejected(t *testing.T) { // Add a wildcard that would match anything (silly but plausible // admin error) and confirm a private IP literal is still blocked // because the literal isn't itself in the allowlist as exact. allow := AllowlistConfig{Domains: []string{"192.168.1.1"}} // The exact-IP-in-allowlist case bypasses the private check; flip // to a NEAR-but-different IP literal that's NOT in the allowlist. allow2 := AllowlistConfig{Domains: []string{"192.168.0.0"}} _, err := ResolveAndCheck(context.Background(), "http://192.168.1.1/", allow2) if err == nil { t.Fatal("expected rejection for private IP literal not in allowlist") } // Sanity: explicit allowlist entry bypasses. _, err = ResolveAndCheck(context.Background(), "http://192.168.1.1/", allow) if err != nil { t.Errorf("expected explicit allowlist entry to bypass; got: %v", err) } } // TestResolveAndCheck_CloudMetadataRejected blocks the well-known // cloud metadata IP via the link-local check. We use a wildcard that // matches the IP-as-hostname so the rejection comes from the // private/link-local layer (not the allowlist). func TestResolveAndCheck_CloudMetadataRejected(t *testing.T) { // "*.169.254.169.254" wildcard wouldn't match either; instead use // a wildcard that matches any IP literal under .254 — but // matchesAllowlist treats '.' as a literal so we just allowlist // the IP itself with a one-bit-different sibling that fails the // exact-allow check (so private check still runs). // // Easier: include a different exact IP entry so the IP literal // fails hostExplicitlyAllowed but passes the wildcard. allow := AllowlistConfig{Domains: []string{"*.169.254.169.254"}} // matches "x.169.254.169.254", not the bare IP // 169.254.169.254 won't match the wildcard pattern either — // switch to a strategy that lets the host pass allowlist but // fails the private check. _ = allow // Use an explicit non-IP-literal hostname (we'd need DNS to point // to 169.254.169.254 which is not feasible). Instead, exercise // the rejectPrivateIP helper directly for the metadata IP since // the public surface only enters that path through resolution. if err := rejectPrivateIP("metadata.test", net.ParseIP("169.254.169.254")); err == nil { t.Fatal("expected rejection for cloud metadata IP") } else if !strings.Contains(err.Error(), "metadata") { t.Errorf("expected metadata error, got: %v", err) } }