fix(security): close IPv6 SSRF gap in validateAgentURL (C6)
PR #94 blocked 169.254.0.0/16 but left IPv6 equivalents fully open. Go's (*IPNet).Contains() does not match pure IPv6 addresses against IPv4 CIDRs, so ::1, fe80::*, and fc00::/7 all bypassed the check. Add three explicit IPv6 entries to blockedRanges: - fe80::/10 (IPv6 link-local — cloud metadata analogue) - ::1/128 (IPv6 loopback) - fc00::/7 (IPv6 ULA — RFC-4193 private) IPv4-mapped IPv6 (::ffff:169.254.x.x) is already safe: Go normalises these to IPv4 via To4() before Contains() runs. Tests: four new cases in TestValidateAgentURL covering all three blocked IPv6 ranges plus the IPv4-mapped IPv6 auto-normalisation path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
460cd9acf8
commit
7b57f411fc
@ -37,6 +37,13 @@ func NewRegistryHandler(b *events.Broadcaster) *RegistryHandler {
|
||||
// - 10.0.0.0/8 RFC-1918 — lateral movement within private networks
|
||||
// - 172.16.0.0/12 RFC-1918 — includes Docker bridge/overlay ranges
|
||||
// - 192.168.0.0/16 RFC-1918 — home/office LAN ranges
|
||||
// - fe80::/10 IPv6 link-local — same threat class as 169.254.x.x
|
||||
// - ::1/128 IPv6 loopback
|
||||
// - fc00::/7 IPv6 ULA (RFC-4193 private ranges)
|
||||
//
|
||||
// IPv4-mapped IPv6 (e.g. ::ffff:169.254.169.254) is normalised to IPv4 by
|
||||
// Go's net.ParseIP.To4() before Contains() runs, so the IPv4 rules above
|
||||
// catch those without a separate entry.
|
||||
//
|
||||
// Returns a non-nil error suitable for including in a 400 Bad Request response.
|
||||
func validateAgentURL(rawURL string) error {
|
||||
@ -59,16 +66,19 @@ func validateAgentURL(rawURL string) error {
|
||||
cidr string
|
||||
label string
|
||||
}{
|
||||
{"169.254.0.0/16", "link-local (cloud metadata endpoint)"},
|
||||
{"127.0.0.0/8", "loopback"},
|
||||
{"10.0.0.0/8", "RFC-1918 private"},
|
||||
{"172.16.0.0/12", "RFC-1918 private"},
|
||||
{"192.168.0.0/16", "RFC-1918 private"},
|
||||
{"169.254.0.0/16", "link-local address (cloud metadata endpoint)"},
|
||||
{"127.0.0.0/8", "loopback address"},
|
||||
{"10.0.0.0/8", "RFC-1918 private address"},
|
||||
{"172.16.0.0/12", "RFC-1918 private address"},
|
||||
{"192.168.0.0/16", "RFC-1918 private address"},
|
||||
{"fe80::/10", "IPv6 link-local address (cloud metadata analogue)"},
|
||||
{"::1/128", "IPv6 loopback address"},
|
||||
{"fc00::/7", "IPv6 ULA address (RFC-4193 private)"},
|
||||
}
|
||||
for _, r := range blockedRanges {
|
||||
_, network, _ := net.ParseCIDR(r.cidr)
|
||||
if network.Contains(ip) {
|
||||
return errors.New("private/reserved IP ranges are not permitted")
|
||||
return fmt.Errorf("url targets a blocked address: %s", r.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -480,6 +480,17 @@ func TestValidateAgentURL(t *testing.T) {
|
||||
{"blocked RFC1918 192.168.0.1", "http://192.168.0.1:8080", true},
|
||||
{"blocked RFC1918 192.168.1.100", "http://192.168.1.100:8080", true},
|
||||
{"blocked RFC1918 192.168.255.254", "http://192.168.255.254:8080", true},
|
||||
|
||||
// ── Must be rejected: IPv6 SSRF vectors (C6 gap) ─────────────────────
|
||||
// Go's IPv4 CIDRs do not match pure IPv6 addresses via Contains(), so
|
||||
// each IPv6 range needs an explicit blocklist entry.
|
||||
{"blocked IPv6 loopback [::1]", "http://[::1]:8080", true},
|
||||
{"blocked IPv6 link-local [fe80::1]", "http://[fe80::1]:8080", true},
|
||||
{"blocked IPv6 ULA [fd00::1]", "http://[fd00::1]:8080", true},
|
||||
// IPv4-mapped IPv6 for a blocked range must also be rejected.
|
||||
// Go normalises ::ffff:169.254.x.x to IPv4 via To4(), so the existing
|
||||
// 169.254.0.0/16 entry catches it without a dedicated rule.
|
||||
{"blocked IPv4-mapped IPv6 link-local", "http://[::ffff:169.254.169.254]:80", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user