mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
* Add DenyLists parsing * Remove unneeded returned error * Plug global denylist into Command * app creation: apply denylist to backchannel logout URI * Inject denylist to backchannel logout worker * webhook config: validate against blocked URLs * Add notificationsWebhook denylist target * command: Add SMTP endpoint validation against blocklist * command: Add SMS endpoint validation against blocklist * Validate webhook endpoint against denylist on channel notification * Remove unused tests * handle deprecated denylists * remove unintended denylist entry in deprecated list * use single http client * fix tests * update comments * fixes * cleanup * address comments * fix merge --------- Co-authored-by: Livio Spring <[email protected]>
160 lines
3.8 KiB
Go
160 lines
3.8 KiB
Go
package denylist
|
|
|
|
import (
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func TestIsURLBlocked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
denyList := []AddressChecker{
|
|
NewHostChecker("192.168.5.0/24"),
|
|
NewHostChecker("127.0.0.1"),
|
|
NewHostChecker("test.com"),
|
|
}
|
|
|
|
type fields struct {
|
|
lookup func(host string) ([]net.IP, error)
|
|
}
|
|
type args struct {
|
|
address *url.URL
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want error
|
|
}{
|
|
{
|
|
name: "in range (CIDR match)",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("192.168.5.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://192.168.5.4/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("192.168.5.0/24"),
|
|
},
|
|
{
|
|
name: "exact ip literal match",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("127.0.0.1"),
|
|
},
|
|
{
|
|
name: "domain string match",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test.com:42/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("test.com"),
|
|
},
|
|
{
|
|
name: "allowed domain string pass",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "unmatched domain resolves to blocked IP (SSRF pivot attempt)",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("127.0.0.1"),
|
|
},
|
|
{
|
|
name: "dns resolver failures are isolated",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return nil, io.EOF
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: zerrors.ThrowInternal(io.EOF, "NET-4m9s2", "lookup failed"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := IsURLBlocked(denyList, tt.args.address, tt.fields.lookup)
|
|
assert.ErrorIs(t, got, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsHostnameBlocked_EdgeCases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
denyList := []AddressChecker{
|
|
NewHostChecker("127.0.0.1"),
|
|
NewHostChecker("blocked.example.com"),
|
|
}
|
|
|
|
t.Run("blocked by host lookup via hostname method", func(t *testing.T) {
|
|
t.Parallel()
|
|
err := IsHostnameBlocked(denyList, "safe.example.com", func(_ string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
|
})
|
|
assert.ErrorIs(t, err, NewAddressDeniedError("127.0.0.1"))
|
|
})
|
|
|
|
t.Run("blocked by exact domain string mapping", func(t *testing.T) {
|
|
t.Parallel()
|
|
err := IsHostnameBlocked(denyList, "blocked.example.com", func(_ string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("192.0.2.1")}, nil
|
|
})
|
|
assert.ErrorIs(t, err, NewAddressDeniedError("blocked.example.com"))
|
|
})
|
|
|
|
t.Run("ipv6 target check compatibility", func(t *testing.T) {
|
|
t.Parallel()
|
|
// URL parsing or raw string handling yields plain IPv6 targets, make sure checker behaves cleanly
|
|
err := IsHostnameBlocked(denyList, "::1", func(_ string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("::1")}, nil
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func mustNewURL(t *testing.T, raw string) *url.URL {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
t.Errorf("unable to parse address of %q because: %v", raw, err)
|
|
t.FailNow()
|
|
}
|
|
return u
|
|
}
|