Public source validation / validate (push) Failing after 3m8s
110 lines
4.1 KiB
Go
110 lines
4.1 KiB
Go
package probe
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/itworx/pulse/internal/audit"
|
|
)
|
|
|
|
type resolverFunc func(context.Context, string) ([]netip.Addr, error)
|
|
|
|
func (f resolverFunc) LookupIP(ctx context.Context, host string) ([]netip.Addr, error) {
|
|
return f(ctx, host)
|
|
}
|
|
|
|
func privatePolicy() NetworkPolicy {
|
|
return NetworkPolicy{Revision: 1, AllowedNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}}
|
|
}
|
|
|
|
func TestNetworkPolicyBlocksSSRFAndAllowsExplicitLAN(t *testing.T) {
|
|
policy := privatePolicy()
|
|
resolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) {
|
|
if host == "lan.internal" {
|
|
return []netip.Addr{netip.MustParseAddr("10.10.0.8")}, nil
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("169.254.169.254")}, nil
|
|
})
|
|
if _, err := policy.ValidateTarget(context.Background(), resolver, "https", "lan.internal", 443); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := policy.ValidateTarget(context.Background(), resolver, "https", "metadata.internal", 443); err == nil {
|
|
t.Fatal("metadata target was allowed")
|
|
}
|
|
if _, err := policy.ValidateTarget(context.Background(), resolver, "https", resolverHost("127.0.0.1"), 80); err == nil {
|
|
t.Fatal("loopback target was allowed")
|
|
}
|
|
}
|
|
|
|
func TestNetworkPolicyRejectsDNSRebindingAndRedirectEscape(t *testing.T) {
|
|
policy := privatePolicy()
|
|
addresses := []netip.Addr{netip.MustParseAddr("10.0.0.2")}
|
|
resolver := resolverFunc(func(_ context.Context, _ string) ([]netip.Addr, error) {
|
|
current := addresses
|
|
addresses = []netip.Addr{netip.MustParseAddr("169.254.169.254")}
|
|
return current, nil
|
|
})
|
|
if _, err := policy.ValidateTarget(context.Background(), resolver, "https", "service.internal", 443); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := policy.ValidateTarget(context.Background(), resolver, "https", "service.internal", 443); err == nil {
|
|
t.Fatal("DNS rebinding to a blocked address was allowed")
|
|
}
|
|
resolver = resolverFunc(func(_ context.Context, _ string) ([]netip.Addr, error) {
|
|
return []netip.Addr{netip.MustParseAddr("10.0.0.2"), netip.MustParseAddr("169.254.169.254")}, nil
|
|
})
|
|
if _, err := policy.ValidateTarget(context.Background(), resolver, "https", "service.internal", 443); err == nil {
|
|
t.Fatal("mixed DNS answer was allowed")
|
|
}
|
|
client, err := NewSafeClient(policy, resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) {
|
|
if host == "safe.internal" {
|
|
return []netip.Addr{netip.MustParseAddr("10.0.0.2")}, nil
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("169.254.169.254")}, nil
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
redirect := httptest.NewRequest(http.MethodGet, "https://metadata.internal/", nil)
|
|
if err := client.HTTP.CheckRedirect(redirect, nil); err == nil {
|
|
t.Fatal("redirect escape was allowed")
|
|
}
|
|
}
|
|
|
|
func TestNetworkPolicyBoundsRequestsResponsesAndAudits(t *testing.T) {
|
|
policy := privatePolicy()
|
|
request := httptest.NewRequest(http.MethodPost, "https://example.com", strings.NewReader("body"))
|
|
if err := policy.ValidateRequest(request); err == nil {
|
|
t.Fatal("POST/body request was allowed")
|
|
}
|
|
request = httptest.NewRequest(http.MethodGet, "https://example.com", nil)
|
|
request.Header.Set("Authorization", "secret")
|
|
if err := policy.ValidateRequest(request); err == nil {
|
|
t.Fatal("authorization header was allowed")
|
|
}
|
|
response := &http.Response{Body: ioNopCloser{Reader: strings.NewReader("12345")}}
|
|
if _, err := ReadLimitedBody(response, 4); err == nil {
|
|
t.Fatal("oversized response was allowed")
|
|
}
|
|
store := &audit.MemoryStore{}
|
|
if err := RecordPolicyChange(context.Background(), store, "user", "policy", "corr", NetworkPolicy{Revision: 1}, policy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(store.Events) != 1 || store.Events[0].Action != "probe.network_policy.update" || store.Events[0].After["maxResponseBytes"] == nil {
|
|
t.Fatalf("audit=%+v", store.Events)
|
|
}
|
|
if err := RecordPolicyChange(context.Background(), nil, "user", "policy", "corr", policy, policy); err == nil {
|
|
t.Fatal("nil audit store was accepted")
|
|
}
|
|
}
|
|
|
|
type ioNopCloser struct{ *strings.Reader }
|
|
|
|
func (ioNopCloser) Close() error { return nil }
|
|
|
|
func resolverHost(host string) string { return host }
|