From f7da69f25fc5ecf122a4d0d45fc6a7f30af7e33b Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Thu, 11 Jun 2026 18:45:46 -0400 Subject: [PATCH] fix(security): SSRF guard gaps + DNS port restriction + metrics auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. SSRF guard now blocks 0.0.0.0/8 (routes to localhost on Linux) and 100.64.0.0/10 (CGNAT). Also rejects unspecified, multicast, and loopback IPs via net.IP methods for defense in depth. 2. DNS monitor type no longer bypasses SSRF guard. The DNSServer address is resolved and validated against isPrivateIP before use. Port restricted to 53 — prevents arbitrary internal port probing via crafted DNSServer values. 3. /metrics now default-deny when MetricsPublic is false, regardless of whether UPTOP_CLUSTER_SECRET is set. Previously, no secret = no auth check = metrics exposed to everyone. --- internal/monitor/checker.go | 23 +++++++++++++++++++---- internal/monitor/safedial.go | 5 +++++ internal/server/server.go | 4 ++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/internal/monitor/checker.go b/internal/monitor/checker.go index b5e9235..1f00dd5 100644 --- a/internal/monitor/checker.go +++ b/internal/monitor/checker.go @@ -63,7 +63,7 @@ func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *htt case "port": return runPortCheck(ctx, site) case "dns": - return runDNSCheck(ctx, site) + return runDNSCheck(ctx, site, private) default: return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "unsupported monitor type: " + site.Type} } @@ -180,7 +180,7 @@ func runPortCheck(_ context.Context, site models.SiteConfig) CheckResult { return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()} } -func runDNSCheck(_ context.Context, site models.SiteConfig) CheckResult { +func runDNSCheck(_ context.Context, site models.SiteConfig, allowPrivate bool) CheckResult { host := site.Hostname if host == "" { host = site.URL @@ -190,9 +190,24 @@ func runDNSCheck(_ context.Context, site models.SiteConfig) CheckResult { if server == "" { server = defaultDNSServer } - if _, _, err := net.SplitHostPort(server); err != nil { - server = net.JoinHostPort(server, defaultDNSPort) + serverHost, serverPort, err := net.SplitHostPort(server) + if err != nil { + serverHost = server + serverPort = defaultDNSPort } + if !allowPrivate { + if serverPort != defaultDNSPort { + return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "DNS server port must be 53"} + } + if ips, err := net.LookupIP(serverHost); err == nil { + for _, ip := range ips { + if isPrivateIP(ip) { + return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "DNS server resolves to private address"} + } + } + } + } + server = net.JoinHostPort(serverHost, serverPort) qtype := dns.TypeA switch site.DNSResolveType { diff --git a/internal/monitor/safedial.go b/internal/monitor/safedial.go index ae8a0b8..5e85ffa 100644 --- a/internal/monitor/safedial.go +++ b/internal/monitor/safedial.go @@ -11,9 +11,11 @@ var privateRanges []*net.IPNet func init() { cidrs := []string{ + "0.0.0.0/8", "127.0.0.0/8", "::1/128", "10.0.0.0/8", + "100.64.0.0/10", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", @@ -27,6 +29,9 @@ func init() { } func isPrivateIP(ip net.IP) bool { + if ip.IsUnspecified() || ip.IsMulticast() || ip.IsLoopback() { + return true + } for _, network := range privateRanges { if network.Contains(ip) { return true diff --git a/internal/server/server.go b/internal/server/server.go index 0e6ebf1..cac166e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -354,8 +354,8 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - if !s.cfg.MetricsPublic && s.cfg.ClusterKey != "" { - if !checkSecret(r.Header.Get("X-Upkeep-Secret"), s.cfg.ClusterKey) { + if !s.cfg.MetricsPublic { + if !s.requireAuth(r) { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } -- 2.52.0