Files
glchat/internal/httpx/safeurl_test.go
T

142 lines
5.5 KiB
Go
Raw Normal View History

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 с проверкой адреса")
}
}