{"record":{"id":"0088b6f0d1640a0e","repo":"iOfficeAI/OfficeCLI","slug":"refusing-to-fetch-what-from-non-public-address","errorCode":null,"errorMessage":"Refusing to fetch {what} from non-public address '{addr}' (host '{host}'). Remote {what} sources must resolve to a public IP (SSRF protection).","messagePattern":"Refusing to fetch (.+?) from non-public address '(.+?)' \\(host '(.+?)'\\)\\. Remote (.+?) sources must resolve to a public IP \\(SSRF protection\\)\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/officecli/Core/SsrfGuard.cs","lineNumber":49,"sourceCode":"    /// ConnectCallback — rather than resolving the hostname up front — also\n    /// closes the DNS-rebinding/TOCTOU window, since the address we vet is the\n    /// address we connect to.\n    /// </summary>\n    /// <param name=\"what\">Noun used in the refusal message, e.g. \"image\" or \"file\".</param>\n    public static SocketsHttpHandler CreateGuardedHandler(string what)\n    {\n        return new SocketsHttpHandler\n        {\n            AllowAutoRedirect = true,\n            MaxAutomaticRedirections = 10,\n            ConnectCallback = async (ctx, ct) =>\n            {\n                var host = ctx.DnsEndPoint.Host;\n                var addresses = await Dns.GetHostAddressesAsync(host, ct).ConfigureAwait(false);\n                foreach (var addr in addresses)\n                {\n                    if (!IsPublicAddress(addr))\n                        throw new ArgumentException(\n                            $\"Refusing to fetch {what} from non-public address '{addr}' (host '{host}'). \" +\n                            $\"Remote {what} sources must resolve to a public IP (SSRF protection).\");\n                }\n                var target = addresses.FirstOrDefault()\n                    ?? throw new ArgumentException($\"Could not resolve host '{host}'.\");\n                var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };\n                try\n                {\n                    await socket.ConnectAsync(target, ctx.DnsEndPoint.Port, ct).ConfigureAwait(false);\n                    return new NetworkStream(socket, ownsSocket: true);\n                }\n                catch\n                {\n                    socket.Dispose();\n                    throw;\n                }\n            }\n        };","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/iOfficeAI/OfficeCLI/blob/1ced45e900782c5083ed550ddf328ee974e425e7/src/officecli/Core/SsrfGuard.cs#L31-L67","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Host the resource on a genuinely public IP / public CDN.","For local files, use a local filesystem path instead of an http://localhost URL.","Use a public tunnel (e.g. a real public ingress) to expose your local asset.","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."],"exampleFix":"// before\npicture = \"http://localhost:8080/logo.png\"   // resolves to 127.0.0.1 -> SSRF refuse\n// after\npicture = \"/abs/path/logo.png\"               // local file, no fetch at all\n// or\npicture = \"https://cdn.example.com/logo.png\" // public CDN","handlingStrategy":"try-catch","validationCode":"// Best-effort pre-check (note: library validates the connect IP to avoid TOCTOU)\nasync Task<bool> ResolvesPublicOnly(string url)\n{\n    var host = new Uri(url).Host;\n    try { return (await System.Net.Dns.GetHostAddressesAsync(host)).All(SsrfGuard.IsPublicAddress); }\n    catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try { bytes = await client.GetByteArrayAsync(url); }\ncatch (System.ArgumentException ex) when (ex.Message.Contains(\"SSRF protection\"))\n{ /* URL resolves to a private/loopback address; use a public source or local path */ }","preventionTips":["Prefer local file paths over http:// for assets you control.","Treat any URL originating from a document or agent instruction as untrusted.","Never disable the guarded handler to 'make it work' locally."],"tags":["network","security","ssrf","http","fetch"],"backgroundTag":null,"analyzedSha":"1ced45e900782c5083ed550ddf328ee974e425e7","analyzedAt":"2026-08-13T13:01:07.193Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}