fix(security): SSRF guard gaps + DNS port restriction + metrics auth #112

Merged
lerko merged 1 commits from fix/security-hardening into main 2026-06-11 23:04:06 +00:00
3 changed files with 26 additions and 6 deletions
+19 -4
View File
@@ -63,7 +63,7 @@ func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *htt
case "port": case "port":
return runPortCheck(ctx, site) return runPortCheck(ctx, site)
case "dns": case "dns":
return runDNSCheck(ctx, site) return runDNSCheck(ctx, site, private)
default: default:
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "unsupported monitor type: " + site.Type} 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()} 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 host := site.Hostname
if host == "" { if host == "" {
host = site.URL host = site.URL
@@ -190,9 +190,24 @@ func runDNSCheck(_ context.Context, site models.SiteConfig) CheckResult {
if server == "" { if server == "" {
server = defaultDNSServer server = defaultDNSServer
} }
if _, _, err := net.SplitHostPort(server); err != nil { serverHost, serverPort, err := net.SplitHostPort(server)
server = net.JoinHostPort(server, defaultDNSPort) 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 qtype := dns.TypeA
switch site.DNSResolveType { switch site.DNSResolveType {
+5
View File
@@ -11,9 +11,11 @@ var privateRanges []*net.IPNet
func init() { func init() {
cidrs := []string{ cidrs := []string{
"0.0.0.0/8",
"127.0.0.0/8", "127.0.0.0/8",
"::1/128", "::1/128",
"10.0.0.0/8", "10.0.0.0/8",
"100.64.0.0/10",
"172.16.0.0/12", "172.16.0.0/12",
"192.168.0.0/16", "192.168.0.0/16",
"169.254.0.0/16", "169.254.0.0/16",
@@ -27,6 +29,9 @@ func init() {
} }
func isPrivateIP(ip net.IP) bool { func isPrivateIP(ip net.IP) bool {
if ip.IsUnspecified() || ip.IsMulticast() || ip.IsLoopback() {
return true
}
for _, network := range privateRanges { for _, network := range privateRanges {
if network.Contains(ip) { if network.Contains(ip) {
return true return true
+2 -2
View File
@@ -354,8 +354,8 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
if !s.cfg.MetricsPublic && s.cfg.ClusterKey != "" { if !s.cfg.MetricsPublic {
if !checkSecret(r.Header.Get("X-Upkeep-Secret"), s.cfg.ClusterKey) { if !s.requireAuth(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized) http.Error(w, "Unauthorized", http.StatusUnauthorized)
return return
} }