{"record":{"id":"c46273d120bd960f","repo":"flipped-aurora/gin-vue-admin","slug":"w-s-c46273","errorCode":null,"errorMessage":"%w: %s","messagePattern":"%w: %s","errorType":"exception","errorClass":"errPrivateAddr","httpStatus":null,"severity":"error","filePath":"server/service/system/sys_timed_task_http.go","lineNumber":37,"sourceCode":"}\n\n// ssrfControl 在拨号阶段(DNS 解析后、连接建立前)校验目标 IP:\n// 每次连接都过检, 天然覆盖重定向与 DNS rebinding(TOCTOU 安全)。\nfunc ssrfControl(allowPrivate bool) func(network, address string, c syscall.RawConn) error {\n\treturn func(_ string, address string, _ syscall.RawConn) error {\n\t\tif allowPrivate {\n\t\t\treturn nil\n\t\t}\n\t\thost, _, err := net.SplitHostPort(address)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"解析拨号地址失败: %w\", err)\n\t\t}\n\t\tip := net.ParseIP(host)\n\t\tif ip == nil {\n\t\t\treturn fmt.Errorf(\"非法拨号 IP: %s\", host)\n\t\t}\n\t\tif isDisallowedIP(ip) {\n\t\t\treturn fmt.Errorf(\"%w: %s\", errPrivateAddr, ip)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// newTimedTaskHTTPClient 定时任务专用 HTTP 客户端:\n// 整体超时 + 禁用环境代理(防经代理绕过 IP 校验) + 拨号层 SSRF 防护\nfunc newTimedTaskHTTPClient(allowPrivate bool, timeout time.Duration) *http.Client {\n\tdialer := &net.Dialer{\n\t\tTimeout: 10 * time.Second,\n\t\tControl: ssrfControl(allowPrivate),\n\t}\n\ttransport := &http.Transport{\n\t\tProxy:       nil, // 显式禁用代理\n\t\tDialContext: dialer.DialContext,\n\t}\n\treturn &http.Client{Timeout: timeout, Transport: transport}\n}","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/service/system/sys_timed_task_http.go#L19-L55","documentation":"The SSRF guard rejects dialing targets whose resolved IP is loopback, private, link-local (unicast/multicast), or unspecified, wrapping the sentinel errPrivateAddr with the offending IP. This blocks timed HTTP tasks from reaching internal infrastructure (SSRF mitigation); the message explicitly tells you the task's 'allow private' option can exempt it.","triggerScenarios":"An HTTP-executor timed task targets a hostname/IP that DNS resolves to 127.0.0.1, ::1, 10.x/172.16-31.x/192.168.x, 169.254.x, or 0.0.0.0 while the task's allowPrivate flag is off.","commonSituations":"Pointing tasks at internal services (http://localhost:8080, http://192.168.1.10) during development; DNS inside the server resolving public-looking names to internal IPs; targets behind corporate NAT; on-host agents listening on loopback.","solutions":["Enable the task's 'allow private network' option if reaching an internal endpoint is intended (security-reviewed)","Target a public endpoint instead of the internal address","If the target should be public but resolves internally, check DNS records / split-horizon DNS for that hostname"],"exampleFix":"// before\n{\"name\":\"ping-internal\",\"executorType\":\"http\",\"httpUrl\":\"http://127.0.0.1:8080/health\",\"allowPrivate\":false}\n// after\n{\"name\":\"ping-internal\",\"executorType\":\"http\",\"httpUrl\":\"http://127.0.0.1:8080/health\",\"allowPrivate\":true} // after security review","handlingStrategy":"validation","validationCode":"addrs, _ := net.LookupHost(u.Hostname())\nfor _, a := range addrs {\n    ip := net.ParseIP(a)\n    if ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()) {\n        // internal target: set allowPrivate or change the endpoint before scheduling\n    }\n}","typeGuard":"func isPublicIP(ip net.IP) bool {\n    return ip != nil && !ip.IsLoopback() && !ip.IsPrivate() && !ip.IsLinkLocalUnicast() && !ip.IsLinkLocalMulticast() && !ip.IsUnspecified()\n}","tryCatchPattern":"resp, err := client.Do(req)\nif err != nil {\n    if errors.Is(err, errPrivateAddr) || strings.Contains(err.Error(), \"SSRF\") {\n        // blocked by SSRF guard: enable allowPrivate (after review) or switch endpoint\n        return\n    }\n    return err\n}","preventionTips":["Treat any 127./10./172.16-31./192.168./169.254. target as intentional only when allowPrivate is set","Document which tasks are allowed to hit internal endpoints and review them periodically","Check DNS resolution from the server host, not your laptop — split-horizon DNS often resolves public names internally","Prefer explicit internal hostnames with allowPrivate over reusing 'localhost' in shared environments"],"tags":["ssrf","security","network","http-client","go"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"3136500ef380842b0eb6c4daa87c3f8a47fcf9e0","analyzedAt":"2026-08-31T13:50:02.721Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}