{"record":{"id":"f1902dcaf5cd7716","repo":"siyuan-note/siyuan","slug":"generated-image-url-resolved-to-a-private-or-inval","errorCode":null,"errorMessage":"generated image URL resolved to a private or invalid IP","messagePattern":"generated image URL resolved to a private or invalid IP","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/openai.go","lineNumber":839,"sourceCode":"\t\t\tif len(via) >= 3 || req.URL.Scheme != \"https\" {\n\t\t\t\treturn errors.New(\"generated image redirect is not allowed\")\n\t\t\t}\n\t\t\treturn CheckHostSSRF(req.URL.Hostname())\n\t\t},\n\t}\n}\n\nfunc generatedImageDialer() *net.Dialer {\n\treturn &net.Dialer{\n\t\tTimeout: 30 * time.Second,\n\t\tControl: func(_, address string, _ syscall.RawConn) error {\n\t\t\thost, _, err := net.SplitHostPort(address)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tip, parseErr := netip.ParseAddr(host)\n\t\t\tif parseErr != nil || isUnsafeGeneratedImageIP(ip.Unmap()) {\n\t\t\t\treturn errors.New(\"generated image URL resolved to a private or invalid IP\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n}\n\nfunc isUnsafeGeneratedImageIP(ip netip.Addr) bool {\n\tif !ip.IsValid() || !ip.IsGlobalUnicast() || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {\n\t\treturn true\n\t}\n\t// IsPrivate 不包含共享地址空间和基准测试网段，这些地址仍可能指向本地基础设施。\n\tfor _, prefix := range []netip.Prefix{\n\t\tnetip.MustParsePrefix(\"100.64.0.0/10\"),\n\t\tnetip.MustParsePrefix(\"198.18.0.0/15\"),\n\t} {\n\t\tif prefix.Contains(ip) {\n\t\t\treturn true\n\t\t}","sourceCodeStart":821,"sourceCodeEnd":857,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/openai.go#L821-L857","documentation":"Thrown by the Control callback of generatedImageDialer (kernel/util/openai.go:839) at socket-connect time, when the resolved IP of the image host is not a safe public unicast address. isUnsafeGeneratedImageIP rejects private (RFC1918), loopback, link-local, unspecified, non-global-unicast, plus the shared (100.64.0.0/10) and benchmark (198.18.0.0/15) ranges. It is the SSRF defense-of-depth layer below CheckHostSSRF, blocking DNS rebinding and IPs that resolve to internal infrastructure.","triggerScenarios":"The image URL's hostname resolves to a private/loopback/link-local IP at connect time; DNS rebinding makes the hostname resolve to a public IP at the SSRF-check stage but a private IP at dial time; the URL points at a hostname that fails to parse as a valid netip.Addr.","commonSituations":"Provider misconfiguration serving images from an internal NAT address; an attacker-crafted image URL targeting internal services; a test environment pointing at localhost; DNS that occasionally returns internal views.","solutions":["Use only reputable image providers whose CDN resolves to public unicast IPs.","Switch to b64_json delivery so no outbound fetch to the URL is attempted.","If self-hosting the provider, ensure its image host announces public IPs and there is no split-horizon DNS.","Do not point the model at user-supplied image URLs."],"exampleFix":"// before\n// adapter relies on URL delivery; provider host intermittently resolves internally\ndata, err = downloadGeneratedImage(requestCtx, result.URL)\n\n// after (force base64 delivery so no dial is made)\nif strings.HasPrefix(strings.ToLower(adapter.model), \"dall-e\") {\n    imageRequest.ResponseFormat = openai.CreateImageResponseFormatB64JSON\n}","handlingStrategy":"fallback","validationCode":"// Strategic pre-check: resolve the host yourself and reject private IPs before dialing\nhost := u.Hostname()\nips, err := net.LookupHost(host)\nif err != nil {\n    return fmt.Errorf(\"cannot resolve image host %s: %s\", host, err)\n}\nfor _, ip := range ips {\n    addr := netip.MustParseAddr(ip)\n    if !addr.IsGlobalUnicast() || addr.IsPrivate() || addr.IsLoopback() || addr.IsLinkLocalUnicast() {\n        return fmt.Errorf(\"image host %s resolves to unsafe IP %s\", host, ip)\n    }\n}","typeGuard":null,"tryCatchPattern":"data, err := downloadGeneratedImage(ctx, result.URL)\nif err != nil && strings.Contains(err.Error(), \"private or invalid IP\") {\n    // SSRF guard fired; do NOT bypass it. Switch to b64_json or refuse the provider.\n    logging.LogErrorf(\"refusing image URL that resolves internally: %s\", result.URL)\n    imageRequest.ResponseFormat = openai.CreateImageResponseFormatB64JSON\n    // ...re-issue CreateImage\n}\nif err != nil { return err }","preventionTips":["Only point image models at reputable providers whose CDN uses public unicast IPs.","Default to b64_json to avoid the dial-time SSRF surface entirely.","Never feed user-supplied image URLs into this path.","Do NOT weaken isUnsafeGeneratedImageIP to work around this — the guard is correct."],"tags":["openai","image-generation","security","ssrf","dns","go"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}