f7008ab86b
validateurl.go is VCS-generic but imported gitea.IsBlockedIP, creating an unexpected generic→Gitea-specific dependency. Extract IsBlockedIP and its CIDR list to internal/netutil/ipcheck.go (a neutral shared package). - gitea/ipcheck.go becomes a thin forwarding wrapper (preserves API compat for callers within the gitea package) - gitea/ipcheck_test.go replaced with a forwarding smoke test; full coverage moves to internal/netutil/ipcheck_test.go - validateurl.go now imports internal/netutil directly
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
// Package netutil provides shared network utilities for review-bot.
|
|
// ipcheck.go implements IP-level SSRF protection by checking resolved addresses
|
|
// against known blocked CIDR ranges (RFC1918, loopback, link-local, etc.).
|
|
package netutil
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
)
|
|
|
|
// blockedCIDRStrings is the canonical list of CIDR strings that should never
|
|
// be contacted by review-bot. See IsBlockedIP for the full list of covered
|
|
// address families.
|
|
//
|
|
// These are hard-coded literals: any parse failure is a programming error.
|
|
// Validity is verified by TestBlockedCIDRsValid in ipcheck_test.go.
|
|
var blockedCIDRStrings = []string{
|
|
// IPv4 loopback
|
|
"127.0.0.0/8",
|
|
// IPv4 unspecified / "this network"
|
|
"0.0.0.0/8",
|
|
// RFC1918 private ranges
|
|
"10.0.0.0/8",
|
|
"172.16.0.0/12",
|
|
"192.168.0.0/16",
|
|
// IPv4 link-local (APIPA, also used by AWS instance metadata 169.254.169.254)
|
|
"169.254.0.0/16",
|
|
// IPv4 shared address space (RFC6598, carrier-grade NAT)
|
|
"100.64.0.0/10",
|
|
// IPv4 multicast
|
|
"224.0.0.0/4",
|
|
// IPv4 reserved / broadcast
|
|
"240.0.0.0/4",
|
|
// IPv6 loopback
|
|
"::1/128",
|
|
// IPv6 unspecified
|
|
"::/128",
|
|
// IPv6 link-local
|
|
"fe80::/10",
|
|
// IPv6 unique local (ULA) — RFC4193
|
|
"fc00::/7",
|
|
// IPv6 multicast
|
|
"ff00::/8",
|
|
}
|
|
|
|
// blockedCIDRs is the parsed form of blockedCIDRStrings.
|
|
// Any entry that fails to parse is recorded in blockedCIDRParseErrors instead
|
|
// of panicking; tests verify this slice is always empty via TestBlockedCIDRsValid.
|
|
var (
|
|
blockedCIDRs []*net.IPNet
|
|
blockedCIDRParseErrors []string
|
|
)
|
|
|
|
func init() {
|
|
blockedCIDRs = make([]*net.IPNet, 0, len(blockedCIDRStrings))
|
|
for _, r := range blockedCIDRStrings {
|
|
_, cidr, err := net.ParseCIDR(r)
|
|
if err != nil {
|
|
// Record the error rather than panicking; TestBlockedCIDRsValid
|
|
// will catch this during tests, and the CI build will fail.
|
|
blockedCIDRParseErrors = append(blockedCIDRParseErrors,
|
|
fmt.Sprintf("ipcheck: invalid built-in CIDR %q: %v", r, err))
|
|
continue
|
|
}
|
|
blockedCIDRs = append(blockedCIDRs, cidr)
|
|
}
|
|
}
|
|
|
|
// BlockedCIDRParseErrors returns any errors encountered parsing the built-in
|
|
// CIDR list. In correct code this will always be empty; tests assert it is.
|
|
func BlockedCIDRParseErrors() []string {
|
|
return blockedCIDRParseErrors
|
|
}
|
|
|
|
// IsBlockedIP reports whether ip is in a blocked address range.
|
|
// It is exported for use by the gitea package's safe dialer, the validate-url
|
|
// subcommand, and tests outside this package.
|
|
//
|
|
// IPv6-mapped IPv4 addresses (e.g. ::ffff:192.168.1.1) are normalized to their
|
|
// IPv4 form before checking so that IPv4 CIDRs catch them.
|
|
//
|
|
// Based on:
|
|
// - RFC1918 private ranges
|
|
// - RFC5735 / RFC4193 special-use IPv4/IPv6 ranges
|
|
// - RFC4291 IPv6 link-local / loopback
|
|
func IsBlockedIP(ip net.IP) bool {
|
|
// Normalize IPv6-mapped IPv4 addresses (::ffff:x.x.x.x) to plain IPv4.
|
|
if v4 := ip.To4(); v4 != nil {
|
|
ip = v4
|
|
}
|
|
for _, cidr := range blockedCIDRs {
|
|
if cidr.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|