fix(security): SSRF guard gaps + DNS port restriction + metrics auth
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.
This commit was merged in pull request #112.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user