iOfficeAI/OfficeCLI · error · ArgumentException

Refusing to fetch {what} from non-public address '{addr}' (h

Error message

Refusing to fetch {what} from non-public address '{addr}' (host '{host}'). Remote {what} sources must resolve to a public IP (SSRF protection).

What it means

Thrown by SsrfGuard.CreateGuardedHandler's ConnectCallback when any resolved IP for the target host is not globally routable (IsPublicAddress false). This protects every remote image/file fetch from SSRF: it blocks loopback, RFC1918 private ranges, link-local incl. 169.254.0.0/16 cloud-metadata, CGNAT 100.64/10, 0.0.0.0/8, multicast/reserved, and IPv6 unique-local/link-local. The check runs in ConnectCallback on the real connect IP, closing the DNS-rebinding/TOCTOU window, and applies to every redirect hop.

Source

Thrown at src/officecli/Core/SsrfGuard.cs:49

    /// ConnectCallback — rather than resolving the hostname up front — also
    /// closes the DNS-rebinding/TOCTOU window, since the address we vet is the
    /// address we connect to.
    /// </summary>
    /// <param name="what">Noun used in the refusal message, e.g. "image" or "file".</param>
    public static SocketsHttpHandler CreateGuardedHandler(string what)
    {
        return new SocketsHttpHandler
        {
            AllowAutoRedirect = true,
            MaxAutomaticRedirections = 10,
            ConnectCallback = async (ctx, ct) =>
            {
                var host = ctx.DnsEndPoint.Host;
                var addresses = await Dns.GetHostAddressesAsync(host, ct).ConfigureAwait(false);
                foreach (var addr in addresses)
                {
                    if (!IsPublicAddress(addr))
                        throw new ArgumentException(
                            $"Refusing to fetch {what} from non-public address '{addr}' (host '{host}'). " +
                            $"Remote {what} sources must resolve to a public IP (SSRF protection).");
                }
                var target = addresses.FirstOrDefault()
                    ?? throw new ArgumentException($"Could not resolve host '{host}'.");
                var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
                try
                {
                    await socket.ConnectAsync(target, ctx.DnsEndPoint.Port, ct).ConfigureAwait(false);
                    return new NetworkStream(socket, ownsSocket: true);
                }
                catch
                {
                    socket.Dispose();
                    throw;
                }
            }
        };

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Host the resource on a genuinely public IP / public CDN.
  2. For local files, use a local filesystem path instead of an http://localhost URL.
  3. Use a public tunnel (e.g. a real public ingress) to expose your local asset.
  4. If you control deployment and must allow an internal host, that is a posture change requiring explicit security review — do not weaken the guard in code without sign-off.

Example fix

// before
picture = "http://localhost:8080/logo.png"   // resolves to 127.0.0.1 -> SSRF refuse
// after
picture = "/abs/path/logo.png"               // local file, no fetch at all
// or
picture = "https://cdn.example.com/logo.png" // public CDN
Defensive patterns

Strategy: try-catch

Validate before calling

// Best-effort pre-check (note: library validates the connect IP to avoid TOCTOU)
async Task<bool> ResolvesPublicOnly(string url)
{
    var host = new Uri(url).Host;
    try { return (await System.Net.Dns.GetHostAddressesAsync(host)).All(SsrfGuard.IsPublicAddress); }
    catch { return false; }
}

Try / catch

try { bytes = await client.GetByteArrayAsync(url); }
catch (System.ArgumentException ex) when (ex.Message.Contains("SSRF protection"))
{ /* URL resolves to a private/loopback address; use a public source or local path */ }

Prevention

When it happens

Trigger: An image=/picture= or data=/media=/model3d= URL whose host resolves to a non-public IP; a redirect chain whose final hop lands on an internal host; an agent-supplied URL crafted to reach 127.0.0.1, 169.254.169.254, or a 10/192.168/172.16-31 address.

Common situations: Pointing at a local dev server (localhost) during testing; an automation/agent tool consuming a URL from an untrusted document; a public domain that DNS-rebinds to an internal address; localhost tunneled services.

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/0088b6f0d1640a0e. Report an issue: GitHub.