{"record":{"id":"d9e5bb4aff8b9d13","repo":"siyuan-note/siyuan","slug":"access-to-private-internal-ip-is-prohibited-d9e5bb","errorCode":null,"errorMessage":"access to private/internal IP is prohibited","messagePattern":"access to private/internal IP is prohibited","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/net.go","lineNumber":187,"sourceCode":"\t\t},\n\t}\n}\n\n// ssrfSafeDialContext 返回智能体出站请求专用的拨号函数：拨号时自行解析主机名并拒绝私网地址，\n// 同时直接连接解析出的公网 IP，使 CheckHostSSRF 的守卫结果与拨号目标一致，\n// 从根上杜绝 DNS 重绑定导致的 TOCTOU 绕过。\n// 与 SSRFSafeDialer 不同，本拨号函数不依赖 SafeMode，始终强制执行。\n// https://github.com/siyuan-note/siyuan/security/advisories/GHSA-x8gv-g2g3-65fj\nfunc ssrfSafeDialContext(timeout time.Duration) func(ctx context.Context, network, addr string) (net.Conn, error) {\n\tdialer := &net.Dialer{Timeout: timeout}\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\thost, port, err := net.SplitHostPort(addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif ip := net.ParseIP(host); ip != nil {\n\t\t\tif isPrivateIP(ip) {\n\t\t\t\treturn nil, errors.New(\"access to private/internal IP is prohibited\")\n\t\t\t}\n\t\t\treturn dialer.DialContext(ctx, network, addr)\n\t\t}\n\t\tips, err := net.DefaultResolver.LookupIPAddr(ctx, host)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar lastErr error\n\t\tfor _, ipAddr := range ips {\n\t\t\tif isPrivateIP(ipAddr.IP) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port))\n\t\t\tif err == nil {\n\t\t\t\treturn conn, nil\n\t\t\t}\n\t\t\tlastErr = err\n\t\t}","sourceCodeStart":169,"sourceCodeEnd":205,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/util/net.go#L169-L205","documentation":"A second dialer variant in net.go resolves the address and, when the host is already a literal IP, refuses any private/internal IP unconditionally (no SafeMode toggle) — a stricter SSRF guard used for contexts that must only reach public hosts.","triggerScenarios":"Dialing a URL whose host is a literal private IP (127.0.0.1, 10.x, 192.168.x, 172.16-31.x, link-local, or IPv6 NAT64/6to4/Teredo embedding private IPv4) through this dialer.","commonSituations":"Local development pointing API endpoints at localhost; internal services behind LAN addresses being fetched by hardened code paths; IPv6 transition addresses embedding private IPv4.","solutions":["Replace the private/literal-IP target with a public hostname","Use the SafeMode-controlled dialer path instead if private access is legitimate","Expose the internal service through a public gateway if external fetching is required"],"exampleFix":"// before\nclient.Get(\"http://192.168.1.10/api\") // blocked\n// after\nclient.Get(\"https://public.example.com/api\")","handlingStrategy":"validation","validationCode":"// pre-check literal IPs before dialing\nip := net.ParseIP(host)\nif ip != nil && (ip.IsPrivate() || ip.IsLoopback()) { return errors.New(\"private target\") }","typeGuard":"func isPublicLiteralIP(host string) bool {\n    ip := net.ParseIP(host)\n    return ip != nil && !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()\n}","tryCatchPattern":"if _, err := hardenedDial(ctx, \"tcp\", addr); err != nil {\n    if strings.Contains(err.Error(), \"private/internal IP\") {\n        return errors.New(\"this code path only supports public hosts\")\n    }\n}","preventionTips":["Never hardcode private IPs into URLs consumed by hardened fetch paths","Use public DNS names for cross-network calls","Remember IPv6 transition addresses (NAT64/6to4/Teredo) are checked too"],"tags":["go","network","security","ssrf"],"backgroundTag":"private-ip-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}