imgproxy ICO Loader RCE, Fixed in 4.0.16 (GHSA-hff9-5pqw-6w9f)
imgproxy 4.0.0 to 4.0.15 allocates ICO data with a wrapping size that unauthenticated crafted-image requests turn into heap corruption; fixed in 4.0.16.
TL;DR
- The ICO image loader in imgproxy 4.0.0 to 4.0.15 reads an attacker-controlled size field and adds 14 to it in a 32-bit sum that wraps. It then allocates far too little memory and writes image data into it, so a crafted ICO crashes the worker or corrupts the heap. The vendor advisory scores it 9.8 Critical, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
- Any caller who can make the proxy process a malicious image can reach the flaw. imgproxy serves image transformations over HTTP and supports URL signing, which is disabled by default. mateofumis and JasonPuglisi disclosed this flaw alongside two related ICO issues.
- The fix shipped in imgproxy 4.0.16 on September 28, 2026, eight days before the vendor advisories on October 6. GitHub added the advisories to its Advisory Database on October 30. The current release is 4.0.20; 4.0.16 and later contain the fix.
- No exploit, no exploitation and no victim are reported, and the advisory is not in CISA's Known Exploited Vulnerabilities (KEV) catalog as of October 31, 2026. Upgrade to 4.0.16 or later now; check how you restrict source URLs while you are in there.
Am I affected?
All of these must hold:
- You run imgproxy 4.0.0 through 4.0.15. Releases from 4.0.16 are fixed.
- Your instance processes attacker-controlled images. This can happen through unrestricted source URLs or user uploads to an allowed source.
- The ICO loader is in use, which it is in the affected releases. The loader registers itself for the ICO format, and the source dispatches on the file's magic bytes.
imgproxy supports signed URLs. Enforcing signatures and controlling which images your application signs can restrict access to the loader; authentication at a reverse proxy can also help. Neither protection fixes the loader or makes malicious images safe. Separately, the memory-allocation companion advisory lists versions 3.3.0 through 4.0.15 as affected: operators on those older versions should also upgrade to 4.0.16 or later.
The vendor released the fix on September 28 and published its advisories on October 6. The GitHub Advisory Database published its records on October 30, 2026. Tools that rely on that database could therefore learn about the flaws later than tools monitoring the vendor directly.
| Branch | Affected versions | Fixed version |
|---|---|---|
| imgproxy 4.x | 4.0.0 through 4.0.15 | 4.0.16 (2026-09-28) |
Check the version you run. The binary prints its own version:
docker exec imgproxy imgproxy --version
The image tag carries it too, but floating tags such as latest need the version command in this section. If you deploy from source, the version constant lives in version/version.go at your checkout's tag.
How do I fix it?
Upgrade to 4.0.16 or later. The current release is 4.0.20.
- In your Compose file, set the imgproxy service image to
darthsim/imgproxy:4.0.20, then pull that configured image and recreate the service:
docker compose pull imgproxy
docker compose up -d imgproxy
- For a binary or source deployment, build or download 4.0.16 or later and restart the service.
Restricting image sources reduces exposure but does not replace the upgrade. IMGPROXY_ALLOWED_SOURCES accepts allowed source URL prefixes, for example:
IMGPROXY_ALLOWED_SOURCES="https://your-bucket.example/"
An allowlist blocks direct fetching from unapproved origins. It does not stop a malicious image uploaded to an allowed bucket, and it is not a general fix for SSRF. Only allow sources whose image contents you trust; upgrade even when source restrictions are enabled.
What is the root cause?
The ICO loader allocates a buffer from an attacker-declared size using a 32-bit addition that can wrap, then writes through a pointer past the end of what was actually allocated. The vulnerable code is vips_foreign_load_ico_header in vips/icoload.c, quoted from the advisory at imgproxy 4.0.15-era commit f4fedbb, lines 154 to 164:
uint32_t data_size = GUINT32_FROM_LE(largest_image_header.data_size);
VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, data_size + BMP_FILE_HEADER_LEN);
void *actual_data = data + BMP_FILE_HEADER_LEN;
if (vips_foreign_load_read_full(ico->source, actual_data, data_size) <= 0) {
vips_error("vips_foreign_load_ico_header", "unable to read ICO image data from the source");
return -1;
}
Three facts in that snippet make the bug. data_size is a 32-bit field the attacker controls through the ICO directory entry. The addition data_size + BMP_FILE_HEADER_LEN, with the header length at 14, happens in 32-bit arithmetic. For data_size = 0xfffffff5 the sum wraps to 3, so the allocation is three bytes. vips_foreign_load_read_full then receives the original data_size, not the wrapped size, and passes it to the Go source callback. The callback copies the bytes available in the source through actual_data to an address 14 bytes after the allocation base, already outside the three-byte buffer.
The advisory's arithmetic for the example: a directory entry with data_size = 0xfffffff5 and data_offset = 22, plus a 14-byte BMP header, gives (0xfffffff5 + 14) mod 2^32 = 3. The entry's 64 bytes of following data get written before the read can report that the declared multi-gigabyte payload is incomplete.
The two companion flaws sit in the same function. The BMP header reconstruction reads num_colors, bpp and height at fixed offsets past a tiny allocation before any size check. That out-of-bounds read becomes an out-of-bounds write of the reconstructed header. And with no validation at all, a 23-byte ICO declaring data_size = 0xfa000068 makes the worker attempt a roughly 4 GB allocation. The unbounded-allocation advisory says that was confirmed on a live production deployment of 4.0.15.
How is it exploited?
No full exploit is public. The advisory publishes the crafted values, and the unbounded-allocation companion includes a proof-of-concept generator script whose output it checksums. The steps come from the advisory's source-to-sink path:
- The attacker hosts a crafted ICO. Its directory entry declares the wrapping size,
data_size = 0xfffffff5withdata_offset = 22, and the file begins with the ICO magic00 00 01 00. - The attacker makes the target's imgproxy process that file: a request for its URL through an instance with unrestricted sources, or a direct upload where a deployment accepts one.
- imgproxy identifies the source as ICO from the magic bytes and dispatches it to
vips_icoload_source_go. vips_foreign_load_ico_headerreads the attacker'sdata_size, allocates the wrapped three bytes, andvips_foreign_load_read_fullpasses the original declared size to the Go callback.- The callback writes the source bytes it has, corrupting the heap. The process either aborts or continues with a corrupted allocator state, which the advisory rates as denial of service or remote code execution.
How does the attack flow?
An unauthenticated request that carries a crafted ICO through the proxy turns a wrapping size field into a heap write before any check can stop it. The missing check is the validation of data_size before the allocation.
data_size 0xfffffff5"] --> B["Request reaches imgproxy
without enforced URL signatures"] B --> C["Source dispatches on magic
00 00 01 00 to the ICO loader"] C --> D["missing check: data_size
never validated"] D --> E["32-bit wrap: allocation of
3 bytes for ~4 GB declared"] E --> F["Read callback writes source bytes
past the allocation"] F --> G["Worker aborts or heap corruption:
crash, potential RCE"]
What can an attacker do?
The attacker crashes the imgproxy worker handling the image, and can repeat that at will, which is a denial of service against the image pipeline. Beyond the crash, the advisory states the heap corruption "may permit remote code execution". Its title says the integer overflow "permits" it, and it scores full confidentiality, integrity and availability impact at CVSS v3.1 9.8. The conditions are the ones in "Am I affected?": an affected version, attacker-influenced image sources, and network reachability. No exploit demonstrating code execution is published, so the demonstrated impact is the crash, and code execution is the advisory's assessment.
The companions add reach of their own. The out-of-bounds read-write in the BMP header reconstruction crashes the worker deterministically, glibc munmap_chunk(): invalid pointer to a Go panic, at CVSS v3.1 7.5. The unbounded allocation exhausts memory from a 23-byte file, also 7.5. Both depend on the attacker being able to submit an image for processing.
No exploitation is reported anywhere as of October 31, 2026, and the advisory is not in the KEV catalog.
Why does the fix work?
Commit a821b6a4cb07, "Fix segfaults during invalid ICO load", released in 4.0.16, validates the size and computes the sum once:
+// Maximum size of an ICO entry data block.
+// Maximum image size in ICO is 256x256 pixels,
+// which gives a maximum of 256 * 256 * 4 bytes for raw RGBA data.
+// 10 MB should be enough for any ICO entry data block,
+// even if an embedded PNG contains metadata.
+// Data blocks larger than this should be considered malicious.
+#define MAX_ICO_ENTRY_DATA_SIZE 10 * 1024 * 1024
+
/**
* ICO ForeignLoad VIPS class implementation (generic)
*/
@@ -153,13 +161,26 @@ vips_foreign_load_ico_header(VipsForeignLoad *load)
uint32_t data_size = GUINT32_FROM_LE(largest_image_header.data_size);
+ // Check if the data size exceeds the maximum allowed ICO entry data size.
+ if (data_size > MAX_ICO_ENTRY_DATA_SIZE) {
+ vips_error("vips_foreign_load_ico_header", "ICO image data is too large");
+ return -1;
+ }
+
// BMP file explicitly excludes BITMAPFILEHEADER, so we need to add it manually.
// We reserve space for it at the beginning of the data buffer.
- VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, data_size + BMP_FILE_HEADER_LEN);
+ uint32_t full_data_size = data_size + BMP_FILE_HEADER_LEN;
+ VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, full_data_size);
+ if (!data) {
+ vips_error("vips_foreign_load_ico_header", "unable to allocate memory for ICO image data");
+ return -1;
+ }
+
void *actual_data = data + BMP_FILE_HEADER_LEN;
if (vips_foreign_load_read_full(ico->source, actual_data, data_size) <= 0) {
vips_error("vips_foreign_load_ico_header", "unable to read ICO image data from the source");
+ VIPS_FREE(data);
return -1;
}
The 10 MB cap removes the wrap and the unbounded allocation in one check. A size that would wrap the 32-bit sum is far over the cap, and it is rejected with an error before any allocation. The single full_data_size variable carries one computed value to every later use, so the size passed downstream can no longer disagree with the size allocated. The null check after VIPS_MALLOC handles a failed allocation instead of dereferencing it, and VIPS_FREE(data) on the error path stops the leak.
The same commit adds the minimum-size check that closes the companion out-of-bounds read-write:
+ // Ensure the data buffer is large enough to contain the BMP header.
+ if (full_data_size < BMP_BITMAP_INFO_HEADER_LEN) {
+ vips_error("vips_foreign_load_ico_header", "ICO image data is too small to contain a valid BMP header");
+ VIPS_FREE(data);
+ return -1;
+ }
+
// Read num_colors and bpp from the BITMAPINFOHEADER
uint32_t num_colors = GUINT32_FROM_LE(*(uint32_t *) (actual_data + 32));
The fix is complete for the ICO entry path in this loader. The size is bounded on entry, the allocation is checked, and the BMP header reconstruction can no longer read or write past a buffer smaller than a header. The loader's test suite gained the malformed ICO fixtures from the advisories, testdata/ico-bad1.ico through ico-bad3.ico, which fail to crash the fixed loader.
How do I check if I was compromised?
No indicators of compromise are published, because no victim or campaign has been reported. Look for the crash signature the advisories describe, which is the observable trace of an attempt:
# Worker restarts and ICO-related aborts in the container log.
docker logs imgproxy --since 720h 2>&1 | grep -iE "SIGABRT|panic|ico" | head -20
A heap-corruption attempt that crashed a worker leaves the abort and a restart behind; a crash is a lead, not proof of code execution. If you suspect the process executed attacker code, treat the container as compromised and rotate everything it could read, which for imgproxy typically includes object-storage credentials in its environment. Review your source logs for requests that carried ICO files from outside your expected origins.
Timeline
- 2026-09-24: imgproxy commits a821b6a4cb07, "Fix segfaults during invalid ICO load".
- 2026-09-28: imgproxy releases v4.0.16, whose changelog line reads "Fixed several vulnerabilities in ICO image loader".
- 2026-10-06: the vendor publishes the three ICO advisories as reviewed GitHub security advisories, GHSA-hff9-5pqw-6w9f, GHSA-494m-4556-wf5p and GHSA-mmq5-xm89-9pfg.
- 2026-10-30: the GitHub Advisory Database review publishes the advisories at 22:57 UTC, making them available to tools that consume that database.
FAQ
Why does the advisory say "may permit" remote code execution?
The demonstrated effect is the crash, and heap corruption leading to code execution is the advisory's assessment of what the bug permits. That assessment is reflected in its 9.8 score, with full impact on confidentiality, integrity and availability. No public exploit carries the corruption to code execution, so the post keeps the advisory's own hedge.
Is 4.0.16 enough, or do I need 4.0.20?
4.0.16 carries all three ICO fixes, so any release from it is safe for these advisories. 4.0.20 is simply the current release, with dependency updates in between. Pin what you test, and pin it at 4.0.16 or later.
My scanner never flagged this. Why?
The three advisories carried no CVE identifiers, and they first reached the GitHub Advisory Database on October 30, 2026, a month after the fix. Tooling that reads only CVE feeds or the database saw nothing before that date, so a clean scan from September does not mean a clean instance. Check the version, not the scan.
Does restricting sources actually help?
Restricting sources blocks direct fetching from unapproved origins. It does not protect a service that processes attacker uploads within an allowed bucket, and it does not fix the loader. Upgrade to 4.0.16 or later.
What about the other two advisories from the same batch?
The batch also carries a heap buffer overflow in the custom BMP loader, GHSA-55vq-g34m-xp8f, and a source-address guard that permits carrier-grade NAT addresses, GHSA-8w7m-q6hg-mfqm. All are fixed in 4.0.16, so one upgrade covers the batch. The BMP loader flaw is the same unauthenticated crash class; the guard flaw is SSRF-adjacent.
Inventory your exposed image pipelines with Ostorlab
Ostorlab's attack surface discovery inventories your internet-facing services. It shows which deployments run an exposed imgproxy endpoint, so you know which instances to upgrade to 4.0.16 or later and which to lock down to trusted sources.
References
- Vendor advisory GHSA-hff9-5pqw-6w9f, "Integer overflow in ICO loader causes remote denial of service and permits remote code execution via heap buffer overflow", CVSS v3.1 9.8, affected 4.0.0 to 4.0.15, patched 4.0.16, credits mateofumis and JasonPuglisi: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-hff9-5pqw-6w9f (checked October 31, 2026)
- Companion advisory GHSA-494m-4556-wf5p, "Out-of-bounds heap read/write in ICO loader BMP header reconstruction", CVSS v3.1 7.5: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-494m-4556-wf5p (checked October 31, 2026)
- Companion advisory GHSA-mmq5-xm89-9pfg, "Unbounded memory allocation in ICO loader", CVSS v3.1 7.5, with the checksummed proof-of-concept generator and the production confirmation on 4.0.15: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-mmq5-xm89-9pfg (checked October 31, 2026)
- Companion advisory GHSA-55vq-g34m-xp8f, the custom BMP loader heap buffer overflow: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-55vq-g34m-xp8f (checked October 31, 2026)
- Companion advisory GHSA-8w7m-q6hg-mfqm, the source-address guard permitting CGNAT addresses: https://github.com/imgproxy/imgproxy/security/advisories/GHSA-8w7m-q6hg-mfqm (checked October 31, 2026)
- Fix commit a821b6a4cb07, "Fix segfaults during invalid ICO load", authored September 24, 2026: https://github.com/imgproxy/imgproxy/commit/a821b6a4cb07 (checked October 31, 2026)
- imgproxy v4.0.16 release, September 28, 2026, changelog line "Fixed several vulnerabilities in ICO image loader": https://github.com/imgproxy/imgproxy/releases/tag/v4.0.16 (checked October 31, 2026)
- The vulnerable
vips/icoload.cat the advisory's cited commit f4fedbb: https://github.com/imgproxy/imgproxy/blob/f4fedbb5c7f6684e7a8e0a9bb7e666773f3c40c0/vips/icoload.c (checked October 31, 2026) - The darthsim/imgproxy Docker image, 36,809,323 pulls: https://hub.docker.com/r/darthsim/imgproxy (checked October 31, 2026)
- CISA Known Exploited Vulnerabilities catalog, JSON feed, searched for the advisories: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (checked October 31, 2026)
- imgproxy URL signing: https://docs.imgproxy.net/usage/signing_url