Replaces #559, which conflicted. Model: opus-5-5
This commit is contained in:
+105
-33
@@ -25,36 +25,64 @@ var (
|
||||
errNoIPs = errors.New(
|
||||
"hostname resolved to no IP addresses",
|
||||
)
|
||||
errBlockedIP = errors.New(
|
||||
"blocked private, reserved or cloud metadata address",
|
||||
// ErrBlockedPrivateOrReservedIP reports an address in the
|
||||
// default blocklist's private and reserved ranges,
|
||||
// blockedNetworks.
|
||||
ErrBlockedPrivateOrReservedIP = errors.New(
|
||||
"blocked private or reserved address",
|
||||
)
|
||||
// errBlockedPublicMetadata reports a public address on the
|
||||
// default blocklist, one in blockedPublicNetworks.
|
||||
errBlockedPublicMetadata = errors.New(
|
||||
"blocked cloud metadata address",
|
||||
)
|
||||
errBlockedMetadata = errors.New(
|
||||
"blocked link-local or cloud instance metadata " +
|
||||
"address: ALLOWED_EGRESS_CIDRS cannot open it",
|
||||
"blocked link-local, cloud instance metadata or " +
|
||||
"unspecified address: ALLOWED_EGRESS_CIDRS cannot open it",
|
||||
)
|
||||
errInvalidScheme = errors.New(
|
||||
"only http and https are allowed",
|
||||
)
|
||||
)
|
||||
|
||||
// blockedNetworks is the default blocklist: the private and
|
||||
// reserved IP ranges, plus the public cloud metadata addresses,
|
||||
// that are blocked to prevent SSRF attacks. An operator can
|
||||
// permit specific blocks out of this set with
|
||||
// ALLOWED_EGRESS_CIDRS; see Guard.
|
||||
// blockedNetworks and blockedPublicNetworks together are the
|
||||
// default blocklist: the private and reserved IP ranges, plus
|
||||
// the public cloud metadata addresses, that are blocked to
|
||||
// prevent SSRF attacks. An operator can permit specific blocks
|
||||
// out of this set with ALLOWED_EGRESS_CIDRS; see Guard.
|
||||
//
|
||||
// blockedNetworks holds the private and reserved IP ranges.
|
||||
//
|
||||
//nolint:gochecknoglobals // package-level network list is appropriate here
|
||||
var blockedNetworks []*net.IPNet
|
||||
|
||||
// alwaysBlockedNetworks are the ranges no configuration can
|
||||
// open: the link-local blocks and the cloud instance metadata
|
||||
// endpoints that live outside them. Reaching one is credential
|
||||
// or user-data theft rather than delivery to an internal
|
||||
// service, so a supplied CIDR that covers such an address still
|
||||
// leaves it blocked.
|
||||
// blockedPublicNetworks holds the default blocklist's public
|
||||
// addresses, kept apart from blockedNetworks so that they are
|
||||
// refused as cloud metadata addresses, never as private or
|
||||
// reserved ones.
|
||||
//
|
||||
// Inclusion criterion — an address belongs here only if BOTH
|
||||
// hold, and every entry below satisfies both:
|
||||
// A public address belongs on the default blocklist only if it
|
||||
// hands credentials, user data or bootstrap material to whatever
|
||||
// can reach it, without the caller presenting anything; it goes
|
||||
// in this list. A provider's other public addresses are not
|
||||
// refused, since reaching them can be legitimate and no list of
|
||||
// them could be complete.
|
||||
//
|
||||
//nolint:gochecknoglobals // package-level network list is appropriate here
|
||||
var blockedPublicNetworks []*net.IPNet
|
||||
|
||||
// alwaysBlockedNetworks are the ranges no configuration can
|
||||
// open, so a supplied CIDR that covers one still leaves it
|
||||
// blocked. An entry is here for one of two reasons: it is a
|
||||
// metadata endpoint (the link-local blocks and the cloud
|
||||
// instance metadata endpoints that live outside them), or it is
|
||||
// an unspecified address. Reaching a metadata endpoint is
|
||||
// credential or user-data theft rather than delivery to an
|
||||
// internal service.
|
||||
//
|
||||
// Inclusion criterion for metadata endpoints — one belongs here
|
||||
// only if BOTH hold, and every metadata entry below satisfies
|
||||
// both:
|
||||
//
|
||||
// 1. It is a fixed address assigned by the provider, or a
|
||||
// range reserved by IANA — never one the operator chose.
|
||||
@@ -65,8 +93,8 @@ var blockedNetworks []*net.IPNet
|
||||
// not cheaply rotated.
|
||||
//
|
||||
// Both halves are load-bearing, so use them to refuse a
|
||||
// candidate and say why. An endpoint disclosing only the
|
||||
// operator's own inventory (instance id, region, disks, NICs)
|
||||
// metadata candidate and say why. An endpoint disclosing only
|
||||
// the operator's own inventory (instance id, region, disks, NICs)
|
||||
// fails (2): letting a delivery target reach the operator's own
|
||||
// infrastructure is the feature ALLOWED_EGRESS_CIDRS exists to
|
||||
// provide. But (2) is not "IAM credentials only" either —
|
||||
@@ -81,12 +109,21 @@ var blockedNetworks []*net.IPNet
|
||||
// when it clears both halves. Nothing in this list can be
|
||||
// reopened, so putting a public address here leaves the operator
|
||||
// no escape hatch at all — the condition ALLOWED_EGRESS_CIDRS
|
||||
// exists to remove. Default-block it in blockedNetworks instead,
|
||||
// which an allowlist can override.
|
||||
// exists to remove. Default-block it in blockedPublicNetworks
|
||||
// instead, which an allowlist can override.
|
||||
//
|
||||
// This is a criterion, not an enumeration of every metadata
|
||||
// address in existence.
|
||||
//
|
||||
// The unspecified addresses 0.0.0.0 and :: are here for a
|
||||
// separate reason: they disclose nothing, but no host can have
|
||||
// either, and on Linux a connection to one reaches this host's
|
||||
// own loopback. Listing them means an allowlist reaches loopback
|
||||
// only through an entry that covers a loopback address
|
||||
// (127.0.0.0/8, ::1/128, 0.0.0.0/0), never through one that
|
||||
// covers only 0.0.0.0 or :: (0.0.0.0/8, for example). Nothing
|
||||
// else lives at either address, so refusing them costs nothing.
|
||||
//
|
||||
// Every entry is either already in blockedNetworks — this list is
|
||||
// what makes it unconditional — or an alternate encoding of
|
||||
// 169.254.169.254 that Contains does not match against
|
||||
@@ -106,23 +143,49 @@ var alwaysBlockedNetworks []*net.IPNet
|
||||
//nolint:gochecknoinits // init is the idiomatic way to parse CIDRs once at startup
|
||||
func init() {
|
||||
blockedNetworks = mustParseCIDRs([]string{
|
||||
// IPv4 loopback.
|
||||
"127.0.0.0/8",
|
||||
// RFC 1918 private network.
|
||||
"10.0.0.0/8",
|
||||
// RFC 1918 private network.
|
||||
"172.16.0.0/12",
|
||||
// RFC 1918 private network.
|
||||
"192.168.0.0/16",
|
||||
// IPv4 link-local.
|
||||
"169.254.0.0/16",
|
||||
// "This network", holding the IPv4 unspecified address 0.0.0.0.
|
||||
"0.0.0.0/8",
|
||||
// Carrier-grade NAT shared address space.
|
||||
"100.64.0.0/10",
|
||||
// IETF protocol assignments.
|
||||
"192.0.0.0/24",
|
||||
// IPv4 documentation (TEST-NET-1).
|
||||
"192.0.2.0/24",
|
||||
// Benchmarking.
|
||||
"198.18.0.0/15",
|
||||
// IPv4 documentation (TEST-NET-2).
|
||||
"198.51.100.0/24",
|
||||
// IPv4 documentation (TEST-NET-3).
|
||||
"203.0.113.0/24",
|
||||
// IPv4 multicast.
|
||||
"224.0.0.0/4",
|
||||
// Reserved, including the broadcast address.
|
||||
"240.0.0.0/4",
|
||||
// IPv6 loopback.
|
||||
"::1/128",
|
||||
// IPv6 unspecified address.
|
||||
"::/128",
|
||||
// IPv6 unique local addresses.
|
||||
"fc00::/7",
|
||||
// IPv6 link-local.
|
||||
"fe80::/10",
|
||||
// IPv6 multicast.
|
||||
"ff00::/8",
|
||||
// IPv6 documentation.
|
||||
"2001:db8::/32",
|
||||
})
|
||||
|
||||
blockedPublicNetworks = mustParseCIDRs([]string{
|
||||
// Azure WireServer, a public address that serves VM credentials.
|
||||
"168.63.129.16/32",
|
||||
})
|
||||
@@ -179,6 +242,14 @@ func init() {
|
||||
// allowlist from opening it.
|
||||
"192.0.0.192/32",
|
||||
|
||||
// The unspecified addresses, each of which reaches this
|
||||
// host's loopback on Linux.
|
||||
//
|
||||
// IPv4 unspecified address, inside the blocked 0.0.0.0/8.
|
||||
"0.0.0.0/32",
|
||||
// IPv6 unspecified address.
|
||||
"::/128",
|
||||
|
||||
// 169.254.169.254 as an IPv4-compatible IPv6 address.
|
||||
"::a9fe:a9fe/128",
|
||||
// 169.254.169.254 behind the NAT64 well-known prefix.
|
||||
@@ -218,13 +289,6 @@ func matchesAny(networks []*net.IPNet, ip net.IP) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isBlockedIP checks whether an IP address falls within
|
||||
// the default blocklist, before any operator allowlist is
|
||||
// considered.
|
||||
func isBlockedIP(ip net.IP) bool {
|
||||
return matchesAny(blockedNetworks, ip)
|
||||
}
|
||||
|
||||
// Guard makes every SSRF decision in the process.
|
||||
//
|
||||
// It holds the operator's ALLOWED_EGRESS_CIDRS allowlist and
|
||||
@@ -322,10 +386,12 @@ func (g *Guard) allows(ip net.IP) bool {
|
||||
// The order is the policy:
|
||||
//
|
||||
// 1. alwaysBlockedNetworks is refused before the allowlist is
|
||||
// consulted, so no configured CIDR reaches link-local or a
|
||||
// cloud metadata endpoint at a non-public address.
|
||||
// consulted, so no configured CIDR reaches link-local, a
|
||||
// cloud metadata endpoint at a non-public address, or an
|
||||
// unspecified address.
|
||||
// 2. The allowlist is consulted next, so a listed private
|
||||
// network becomes reachable.
|
||||
// network, or a listed public address on the default
|
||||
// blocklist, becomes reachable.
|
||||
// 3. Everything else keeps the default blocklist's answer.
|
||||
func (g *Guard) checkIP(ip net.IP) error {
|
||||
if matchesAny(alwaysBlockedNetworks, ip) {
|
||||
@@ -338,9 +404,15 @@ func (g *Guard) checkIP(ip net.IP) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isBlockedIP(ip) {
|
||||
if matchesAny(blockedNetworks, ip) {
|
||||
return fmt.Errorf(
|
||||
"target IP %s: %w", ip, errBlockedIP,
|
||||
"target IP %s: %w", ip, ErrBlockedPrivateOrReservedIP,
|
||||
)
|
||||
}
|
||||
|
||||
if matchesAny(blockedPublicNetworks, ip) {
|
||||
return fmt.Errorf(
|
||||
"target IP %s: %w", ip, errBlockedPublicMetadata,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user