siyuan-note/siyuan · error

generated image URL resolved to a private or invalid IP

Error message

generated image URL resolved to a private or invalid IP

What it means

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.

Source

Thrown at kernel/util/openai.go:839

			if len(via) >= 3 || req.URL.Scheme != "https" {
				return errors.New("generated image redirect is not allowed")
			}
			return CheckHostSSRF(req.URL.Hostname())
		},
	}
}

func generatedImageDialer() *net.Dialer {
	return &net.Dialer{
		Timeout: 30 * time.Second,
		Control: func(_, address string, _ syscall.RawConn) error {
			host, _, err := net.SplitHostPort(address)
			if err != nil {
				return err
			}
			ip, parseErr := netip.ParseAddr(host)
			if parseErr != nil || isUnsafeGeneratedImageIP(ip.Unmap()) {
				return errors.New("generated image URL resolved to a private or invalid IP")
			}
			return nil
		},
	}
}

func isUnsafeGeneratedImageIP(ip netip.Addr) bool {
	if !ip.IsValid() || !ip.IsGlobalUnicast() || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
		return true
	}
	// IsPrivate 不包含共享地址空间和基准测试网段,这些地址仍可能指向本地基础设施。
	for _, prefix := range []netip.Prefix{
		netip.MustParsePrefix("100.64.0.0/10"),
		netip.MustParsePrefix("198.18.0.0/15"),
	} {
		if prefix.Contains(ip) {
			return true
		}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Use only reputable image providers whose CDN resolves to public unicast IPs.
  2. Switch to b64_json delivery so no outbound fetch to the URL is attempted.
  3. If self-hosting the provider, ensure its image host announces public IPs and there is no split-horizon DNS.
  4. Do not point the model at user-supplied image URLs.

Example fix

// before
// adapter relies on URL delivery; provider host intermittently resolves internally
data, err = downloadGeneratedImage(requestCtx, result.URL)

// after (force base64 delivery so no dial is made)
if strings.HasPrefix(strings.ToLower(adapter.model), "dall-e") {
    imageRequest.ResponseFormat = openai.CreateImageResponseFormatB64JSON
}
Defensive patterns

Strategy: fallback

Validate before calling

// Strategic pre-check: resolve the host yourself and reject private IPs before dialing
host := u.Hostname()
ips, err := net.LookupHost(host)
if err != nil {
    return fmt.Errorf("cannot resolve image host %s: %s", host, err)
}
for _, ip := range ips {
    addr := netip.MustParseAddr(ip)
    if !addr.IsGlobalUnicast() || addr.IsPrivate() || addr.IsLoopback() || addr.IsLinkLocalUnicast() {
        return fmt.Errorf("image host %s resolves to unsafe IP %s", host, ip)
    }
}

Try / catch

data, err := downloadGeneratedImage(ctx, result.URL)
if err != nil && strings.Contains(err.Error(), "private or invalid IP") {
    // SSRF guard fired; do NOT bypass it. Switch to b64_json or refuse the provider.
    logging.LogErrorf("refusing image URL that resolves internally: %s", result.URL)
    imageRequest.ResponseFormat = openai.CreateImageResponseFormatB64JSON
    // ...re-issue CreateImage
}
if err != nil { return err }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/f1902dcaf5cd7716. Report an issue: GitHub.