fix(uptime): allow RFC 1918 IPs for admin-configured monitors

HTTP/HTTPS uptime monitors targeting LAN addresses (192.168.x.x,
10.x.x.x, 172.16.x.x) permanently reported 'down' on fresh installs
because SSRF protection rejects RFC 1918 ranges at two independent
checkpoints: the URL validator (DNS-resolution layer) and the safe
dialer (TCP-connect layer). Fixing only one layer leaves the monitor
broken in practice.

- Add IsRFC1918() predicate to the network package covering only the
  three RFC 1918 CIDRs; 169.254.x.x (link-local / cloud metadata)
  and loopback are intentionally excluded
- Add WithAllowRFC1918() functional option to both SafeHTTPClient and
  ValidationConfig; option defaults to false so existing behaviour is
  unchanged for every call site except uptime monitors
- In uptime_service.go, pass WithAllowRFC1918() to both
  ValidateExternalURL and NewSafeHTTPClient together; a coordinating
  comment documents that both layers must be relaxed as a unit
- 169.254.169.254 and the full 169.254.0.0/16 link-local range remain
  unconditionally blocked; the cloud-metadata error path is preserved
- 21 new tests across three packages, including an explicit regression
  guard that confirms RFC 1918 blocks are still applied without the
  option set (TestValidateExternalURL_RFC1918BlockedByDefault)

Fixes issues 6 and 7 from the fresh-install bug report.
This commit is contained in:
GitHub Actions
2026-03-17 21:21:59 +00:00
parent dc9bbacc27
commit 00a18704e8
8 changed files with 1392 additions and 0 deletions

View File

@@ -120,6 +120,14 @@ type ValidationConfig struct {
MaxRedirects int
Timeout time.Duration
BlockPrivateIPs bool
// AllowRFC1918 permits addresses in the RFC 1918 private ranges
// (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
//
// SECURITY NOTE: Must only be set for admin-configured features such as uptime
// monitors. Link-local (169.254.x.x), loopback, cloud metadata, and all other
// restricted ranges remain blocked regardless of this flag.
AllowRFC1918 bool
}
// ValidationOption allows customizing validation behavior.
@@ -145,6 +153,15 @@ func WithMaxRedirects(maxRedirects int) ValidationOption {
return func(c *ValidationConfig) { c.MaxRedirects = maxRedirects }
}
// WithAllowRFC1918 permits addresses in the RFC 1918 private ranges
// (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
//
// Use only for admin-configured features (e.g., uptime monitors targeting internal hosts).
// All other SSRF protections remain active.
func WithAllowRFC1918() ValidationOption {
return func(c *ValidationConfig) { c.AllowRFC1918 = true }
}
// ValidateExternalURL validates a URL for external HTTP requests with comprehensive SSRF protection.
// This function provides defense-in-depth against Server-Side Request Forgery attacks by:
// 1. Validating URL format and scheme
@@ -272,11 +289,23 @@ func ValidateExternalURL(rawURL string, options ...ValidationOption) (string, er
if ip.To4() != nil && ip.To16() != nil && isIPv4MappedIPv6(ip) {
// Extract the IPv4 address from the mapped format
ipv4 := ip.To4()
// Allow RFC 1918 IPv4-mapped IPv6 only when the caller has explicitly opted in.
if config.AllowRFC1918 && network.IsRFC1918(ipv4) {
continue
}
if network.IsPrivateIP(ipv4) {
return "", fmt.Errorf("connection to private ip addresses is blocked for security (detected IPv4-mapped IPv6: %s)", ip.String())
}
}
// Allow RFC 1918 addresses only when the caller has explicitly opted in
// (e.g., admin-configured uptime monitors targeting internal hosts).
// Link-local (169.254.x.x), loopback, cloud metadata, and all other
// restricted ranges remain blocked regardless of this flag.
if config.AllowRFC1918 && network.IsRFC1918(ip) {
continue
}
// Check if IP is in private/reserved ranges using centralized network.IsPrivateIP
// This includes:
// - RFC 1918 private networks (10.x, 172.16.x, 192.168.x)