package httpx import ( "net" "net/http" "net/url" "strings" "testing" "time" ) // TestIsBlockedIP покрывает адреса, по которым сервер не должен ходить сам // (AGENT.md 9.2, 9.7): внутренние сети, метаданные облака, multicast. func TestIsBlockedIP(t *testing.T) { blocked := []string{ "127.0.0.1", "127.9.9.9", "::1", "10.0.0.1", "172.16.0.1", "172.31.255.255", "192.168.1.1", "169.254.169.254", "fe80::1", "0.0.0.0", "::", "0.1.2.3", "224.0.0.1", "ff02::1", "100.64.0.1", "100.127.255.255", "192.0.0.1", "fc00::1", "fd12:3456::1", "::ffff:10.0.0.1", "::ffff:127.0.0.1", "2001:db8::1", } for _, raw := range blocked { ip := net.ParseIP(raw) if ip == nil { t.Fatalf("не разобран адрес %q", raw) } if !IsBlockedIP(ip) { t.Errorf("IsBlockedIP(%s) = false, ожидалось true", raw) } } allowed := []string{"8.8.8.8", "1.1.1.1", "138.16.226.159", "2606:4700::1111", "100.63.255.255"} for _, raw := range allowed { ip := net.ParseIP(raw) if ip == nil { t.Fatalf("не разобран адрес %q", raw) } if IsBlockedIP(ip) { t.Errorf("IsBlockedIP(%s) = true, ожидалось false", raw) } } if !IsBlockedIP(nil) { t.Error("IsBlockedIP(nil) = false, ожидалось true") } } func TestValidatePublicURL(t *testing.T) { cases := []struct { name string raw string allowHTTP bool wantErr bool }{ {name: "https", raw: "https://example.com/page?a=1"}, {name: "http запрещён", raw: "http://example.com/", wantErr: true}, {name: "http разрешён флагом", raw: "http://example.com/", allowHTTP: true}, {name: "file", raw: "file:///etc/passwd", wantErr: true}, {name: "gopher", raw: "gopher://example.com/", wantErr: true}, {name: "javascript", raw: "javascript:alert(1)", wantErr: true}, {name: "userinfo", raw: "https://user:pass@example.com/", wantErr: true}, {name: "пусто", raw: " ", wantErr: true}, {name: "loopback", raw: "https://127.0.0.1/x", wantErr: true}, {name: "метаданные", raw: "https://169.254.169.254/latest/meta-data/", wantErr: true}, {name: "приватная сеть", raw: "https://10.1.2.3/", wantErr: true}, {name: "ipv6 loopback", raw: "https://[::1]/", wantErr: true}, {name: "слишком длинный", raw: "https://example.com/" + strings.Repeat("a", 2100), wantErr: true}, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { parsed, err := ValidatePublicURL(testCase.raw, testCase.allowHTTP) if testCase.wantErr { if err == nil { t.Fatalf("ValidatePublicURL(%q) = %v, ожидалась ошибка", testCase.raw, parsed) } return } if err != nil { t.Fatalf("ValidatePublicURL(%q) вернул ошибку: %v", testCase.raw, err) } }) } } // TestSafeDialControl проверяет, что проверка идёт по уже разрешённому адресу: // именно так закрывается DNS rebinding. func TestSafeDialControl(t *testing.T) { blocked := []string{"127.0.0.1:443", "[::1]:443", "169.254.169.254:80", "10.0.0.5:8080"} for _, address := range blocked { if err := SafeDialControl("tcp", address, nil); err == nil { t.Errorf("SafeDialControl пропустил %s", address) } } if err := SafeDialControl("tcp", "93.184.216.34:443", nil); err != nil { t.Fatalf("SafeDialControl отклонил публичный адрес: %v", err) } // Имя вместо адреса означает, что резолв не произошёл: доверять нельзя. if err := SafeDialControl("tcp", "example.com:443", nil); err == nil { t.Fatal("SafeDialControl принял неразрешённое имя хоста") } } func TestSafeClientBlocksUnsafeRedirects(t *testing.T) { client := SafeClient(time.Second) origin := &http.Request{URL: &url.URL{Scheme: "https", Host: "example.com"}} via := []*http.Request{origin} redirect := &http.Request{URL: &url.URL{Scheme: "http", Host: "127.0.0.1:8080", Path: "/steal"}} if err := client.CheckRedirect(redirect, via); err == nil { t.Fatal("CheckRedirect пропустил переход на внутренний адрес") } // Схема http запрещена всегда: редирект не должен понижать защиту. redirect = &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}} if err := client.CheckRedirect(redirect, via); err == nil { t.Fatal("CheckRedirect пропустил переход на http") } redirect = &http.Request{URL: &url.URL{Scheme: "https", Host: "example.com", Path: "/next"}} if err := client.CheckRedirect(redirect, via); err != nil { t.Fatalf("CheckRedirect отклонил обычный https-редирект: %v", err) } // Больше трёх переходов — отказ (защита от цепочек). many := []*http.Request{origin, origin, origin, origin} if err := client.CheckRedirect(redirect, many); err == nil { t.Fatal("CheckRedirect пропустил слишком длинную цепочку") } } func TestSafeTransportHasNoProxy(t *testing.T) { transport := SafeTransport(time.Second) if transport.Proxy != nil { t.Fatal("транспорт не должен использовать прокси из окружения") } if transport.DialContext == nil { t.Fatal("транспорт должен задавать DialContext с проверкой адреса") } }