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

The download uses a custom DialContext (Control/dialer) that resolves the host and inspects the resulting IP with netip.ParseAddr + isUnsafeGeneratedImageIP. If the host does not resolve to a parseable public IP, or resolves to a private/loopback/link-local/invalid address, the connection is refused with this error. This prevents SSRF: a provider-controlled URL must not be able to reach the machine's own network.

Solutions

  1. Ensure the image host is a public DNS name resolving to a public IP
  2. Fix DNS so the hostname resolves (check resolv.conf / network connectivity)
  3. If using a self-hosted image service on the LAN, expose it via a public HTTPS endpoint instead of a private IP URL
  4. Check the resolved IPs with `dig <host>`; if private, the provider response is untrustworthy — regenerate the image

Example fix

// before
url := "https://intranet.local/img.png" // resolves to 192.168.1.10 -> blocked
// after
url := "https://cdn.example.com/img.png" // public IP -> allowed
Defensive patterns

Strategy: validation

Validate before calling

ip, err := netip.ParseAddr(host)
if err != nil || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
    return errors.New("host resolves to unsafe IP")
}

Prevention

When it happens

Trigger: The image URL hostname resolves to 127.0.0.1, 10.x/172.16.x/192.168.x, ::1, link-local 169.254.x, or fails to resolve (ParseAddr/lookup error) when the dialer connects.

Common situations: Provider returning a URL pointing at localhost or a LAN host (malicious or misconfigured); DNS rebinding to a private IP; DNS resolution failure in an offline/air-gapped environment; IPv6-only host with an address the check treats as unsafe; typo'd internal hostname in a self-hosted proxy config.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/f1902dcaf5cd7716. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/openai.go:986

			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 9f775e8a12)