{"record":{"id":"abbb1535007d9de8","repo":"iOfficeAI/OfficeCLI","slug":"remote-image-exceeds-ssrfguard-maxremotebytes","errorCode":null,"errorMessage":"Remote image exceeds {SsrfGuard.MaxRemoteBytes / (1024 * 1024)} MB limit.","messagePattern":"Remote image exceeds (.+?) MB limit\\.","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/officecli/Core/ImageSource.cs","lineNumber":164,"sourceCode":"    private static (Stream, PartTypeInfo) ResolveUrl(string url)\n    {\n        // SSRF guard lives in the shared SsrfGuard so image and file fetch can\n        // never diverge in policy. See SsrfGuard for the connect-time / redirect\n        // / DNS-rebinding rationale.\n        var handler = SsrfGuard.CreateGuardedHandler(\"image\");\n\n        using var client = new HttpClient(handler, disposeHandler: true) { Timeout = TimeSpan.FromSeconds(30) };\n        client.DefaultRequestHeaders.Add(\"User-Agent\", \"OfficeCLI\");\n\n        var response = client.GetAsync(url).GetAwaiter().GetResult();\n        response.EnsureSuccessStatusCode();\n\n        // Enforce the shared size cap whether or not the server sends\n        // Content-Length. ReadBounded lives in SsrfGuard so image and file\n        // fetch share one limit (see SsrfGuard.MaxRemoteBytes).\n        var declared = response.Content.Headers.ContentLength;\n        if (declared is > SsrfGuard.MaxRemoteBytes)\n            throw new ArgumentException($\"Remote image exceeds {SsrfGuard.MaxRemoteBytes / (1024 * 1024)} MB limit.\");\n        var bytes = SsrfGuard.ReadBounded(response.Content.ReadAsStream(), SsrfGuard.MaxRemoteBytes, url, \"image\");\n        var stream = new MemoryStream(bytes);\n\n        // Try content-type header first\n        var serverMime = response.Content.Headers.ContentType?.MediaType;\n        if (!string.IsNullOrEmpty(serverMime) && TryMimeToContentType(serverMime, out var ct))\n            return (stream, ct);\n\n        // Fallback: extract extension from URL path (strip query string)\n        var uri = new Uri(url);\n        var ext = Path.GetExtension(uri.AbsolutePath);\n        if (!string.IsNullOrEmpty(ext))\n            return (stream, ExtensionToContentType(ext));\n\n        // Last resort: sniff magic bytes\n        if (TrySniffContentType(bytes, out var sniffed))\n            return (stream, sniffed);\n","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/iOfficeAI/OfficeCLI/blob/1ced45e900782c5083ed550ddf328ee974e425e7/src/officecli/Core/ImageSource.cs#L146-L182","documentation":"Thrown by ImageSource.ResolveUrl when a remote image download exceeds the shared size cap defined in SsrfGuard.MaxRemoteBytes (100 MB). The check fires in two places: first against the Content-Length response header (if the server declares it), and second during streaming via SsrfGuard.ReadBounded which enforces the cap even when Content-Length is absent. The limit is shared between image fetch and file fetch so policy cannot diverge.","triggerScenarios":"Calling ImageSource.Resolve with an HTTP(S) URL pointing to an image larger than 100 MB. The declared Content-Length header value exceeds MaxRemoteBytes, or the streamed bytes exceed the cap during ReadBounded. The SSRF guard handler wraps the HttpClient for DNS-rebinding and redirect protection, and the size cap is enforced on top.","commonSituations":"Linking to a high-resolution stock photo or satellite imagery that exceeds 100 MB. A URL that redirects to a much larger payload than expected. A misconfigured server returning an incorrect Content-Length or no Content-Length with a very large body.","solutions":["Download the image locally, resize/compress it to under 100 MB, and embed the local file instead.","Use a URL that points to a smaller version or thumbnail of the image.","If the limit is genuinely too low for your use case, host a pre-resized image at a different URL."],"exampleFix":"// before — remote image too large\nadd image src='https://example.com/huge-tiff-200mb.tif' path='/body'\n\n// after — download, compress, embed locally\n// wget -O /tmp/img.tif 'https://example.com/huge-tiff-200mb.tif'\n// magick /tmp/img.tif -resize 50% -compress LZW /tmp/img-small.tif\nadd image src='/tmp/img-small.tif' path='/body'","handlingStrategy":"validation","validationCode":"// Pre-check: HEAD the URL to get Content-Length before full download\nusing var client = new HttpClient();\nvar headResp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));\nif (headResp.Content.Headers.ContentLength is long len && len > SsrfGuard.MaxRemoteBytes)\n{\n    Console.Error.WriteLine($\"Remote image is {len / (1024*1024)} MB, exceeds the {SsrfGuard.MaxRemoteBytes / (1024*1024)} MB limit.\");\n    return;\n}","typeGuard":null,"tryCatchPattern":"try\n{\n    var (stream, contentType) = ImageSource.Resolve(url);\n}\ncatch (ArgumentException ex) when (ex.Message.Contains(\"MB limit\"))\n{\n    // Download locally, compress/resize, embed as local file\n}","preventionTips":["Pre-check the image size with a HEAD request before embedding from a URL.","Compress or resize large images before embedding them into Office documents.","Prefer local file embedding for images larger than a few MB to avoid network-related failures."],"tags":["image","remote-fetch","size-limit","ssrf","network"],"backgroundTag":null,"analyzedSha":"1ced45e900782c5083ed550ddf328ee974e425e7","analyzedAt":"2026-08-13T13:01:07.193Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}