Files
zitadel/internal/denylist/host_checker.go
T
8e82ec1cb9 Merge commit from fork
* 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]>
2026-06-15 15:36:14 +02:00

90 lines
2.8 KiB
Go

package denylist
import (
"net"
"net/url"
"strings"
internal_net "github.com/zitadel/zitadel/internal/net"
)
var _ AddressChecker = (*HostChecker)(nil)
type HostChecker struct {
Net *net.IPNet
IP net.IP
Domain string
}
// NewHostChecker returns an [AddressChecker].
//
// If the input string is neither a network, nor an IP address, it is considered a domain and saved as such
func NewHostChecker(entry string) AddressChecker {
if entry == "" {
return nil
}
_, network, err := net.ParseCIDR(entry)
if err == nil {
return &HostChecker{Net: network}
}
if ip := net.ParseIP(entry); ip != nil {
return &HostChecker{IP: ip}
}
return &HostChecker{Domain: entry}
}
// IsDenied checks if the address or one of the IPs is blocked by the [HostChecker].
// The address is checked using string comparison.
// If either is blocked, an [AddressDeniedError] will be returned.
// If either is not blocked, it will return nil.
func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
// if the address matches the domain, no additional checks as needed
if c.Domain != "" && strings.EqualFold(c.Domain, address) {
return NewAddressDeniedError(c.Domain)
}
// otherwise we need to check on ips (incl. the resolved ips of the host)
for _, ip := range ips {
if c.Net != nil && c.Net.Contains(ip) {
return NewAddressDeniedError(c.Net.String())
}
if c.IP != nil && c.IP.Equal(ip) {
return NewAddressDeniedError(c.IP.String())
}
}
return nil
}
// IsHostBlocked checks address against denyList. If a match is found, an [AddressDeniedError] will be returned.
// If no match is found, it will return nil.
func IsHostBlocked(denyList []AddressChecker, host string, ips ...net.IP) error {
for _, denied := range denyList {
if err := denied.IsDenied(ips, host); err != nil {
return err
}
}
return nil
}
// IsHostnameBlocked checks a hostname against the denyList. If a match is found, an [AddressDeniedError] will be returned
// It takes an input lookupFunc to convert the hostname to a list of IP addresses.
// If lookupFunc is nil, it defaults to [net.LookupIP]
func IsHostnameBlocked(denyList []AddressChecker, hostname string, lookupFunc internal_net.IPLookupFunc) error {
if lookupFunc == nil {
lookupFunc = net.LookupIP
}
ips, err := internal_net.HostnameToIPList(hostname, lookupFunc)
if err != nil {
return err
}
return IsHostBlocked(denyList, hostname, ips...)
}
// IsURLBlocked checks a URL against denyList. If a match is found, an [AddressDeniedError] will be returned
// It takes an input lookupFunc to convert the URL's hostname to a list of IP addresses.
// If lookupFunc is nil, it defaults to [net.LookupIP]
func IsURLBlocked(denyList []AddressChecker, address *url.URL, lookupFunc internal_net.IPLookupFunc) error {
return IsHostnameBlocked(denyList, address.Hostname(), lookupFunc)
}