Просмотр исходного кода

fix(panel): stop one poisoned DNS answer from blocking outbound tests

SanitizePublicHTTPURL rejected a hostname as soon as any single resolved
address was blocked, so a resolver returning a bogon AAAA for the test URL
host (e.g. 2001::1 for www.google.com, inside the Teredo range blocked
since b51f0976) failed the outbound Check button outright — including TCP
mode, which never uses the test URL. Mirror SSRFGuardedDialContext instead:
one usable address is enough, because the guarded dialer skips blocked
answers at connect time; a hostname with nothing usable is still refused.

Closes #6290
Sanaei 11 часов назад
Родитель
Сommit
2d30ab3ada
2 измененных файлов с 45 добавлено и 3 удалено
  1. 9 3
      internal/web/service/url_safety.go
  2. 36 0
      internal/web/service/url_safety_test.go

+ 9 - 3
internal/web/service/url_safety.go

@@ -72,15 +72,21 @@ func rejectPrivateHost(ctx context.Context, hostname string) error {
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("cannot resolve host %s: %w", hostname, err)
 		return fmt.Errorf("cannot resolve host %s: %w", hostname, err)
 	}
 	}
+	return rejectAllBlockedIPs(hostname, ips)
+}
+
+// One usable address is enough — SSRFGuardedDialContext skips blocked ones at
+// dial time, so a poisoned AAAA answer must not veto a healthy hostname.
+func rejectAllBlockedIPs(hostname string, ips []net.IPAddr) error {
 	if len(ips) == 0 {
 	if len(ips) == 0 {
 		return fmt.Errorf("host %s has no IP addresses", hostname)
 		return fmt.Errorf("host %s has no IP addresses", hostname)
 	}
 	}
 	for _, ipAddr := range ips {
 	for _, ipAddr := range ips {
-		if isBlockedIP(ipAddr.IP) {
-			return fmt.Errorf("host %s resolves to blocked private/internal address %s", hostname, ipAddr.IP.String())
+		if !isBlockedIP(ipAddr.IP) {
+			return nil
 		}
 		}
 	}
 	}
-	return nil
+	return fmt.Errorf("host %s resolves to blocked private/internal address %s", hostname, ips[0].IP.String())
 }
 }
 
 
 func isBlockedIP(ip net.IP) bool {
 func isBlockedIP(ip net.IP) bool {

+ 36 - 0
internal/web/service/url_safety_test.go

@@ -0,0 +1,36 @@
+package service
+
+import (
+	"net"
+	"testing"
+)
+
+func TestRejectAllBlockedIPsNeedsOnlyOneUsableAddress(t *testing.T) {
+	teredo := net.IPAddr{IP: net.ParseIP("2001::1")}
+	public4 := net.IPAddr{IP: net.ParseIP("142.250.74.36")}
+	private4 := net.IPAddr{IP: net.ParseIP("10.0.0.1")}
+
+	cases := []struct {
+		name    string
+		ips     []net.IPAddr
+		wantErr string
+	}{
+		{"poisoned AAAA next to healthy A", []net.IPAddr{teredo, public4}, ""},
+		{"all blocked", []net.IPAddr{teredo, private4}, "host h.example resolves to blocked private/internal address 2001::1"},
+		{"no addresses", nil, "host h.example has no IP addresses"},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := rejectAllBlockedIPs("h.example", tc.ips)
+			if tc.wantErr == "" {
+				if err != nil {
+					t.Fatalf("rejectAllBlockedIPs() = %v, want nil", err)
+				}
+				return
+			}
+			if err == nil || err.Error() != tc.wantErr {
+				t.Fatalf("rejectAllBlockedIPs() = %v, want %q", err, tc.wantErr)
+			}
+		})
+	}
+}