Bigger Than It Says: Finding a Decompression Bomb in a PNG Header
The challenge
Every night at 02:00 the avatar thumbnailer falls over on one file and takes the whole worker pool with it. The file is 46 KB on disk, it is a valid PNG, it opens in a browser, and the upload log records it as a 64 by 64 icon. Nothing about the size on disk explains a machine running out of memory. A PNG carries its pixel dimensions in the header, in plain view, long before a decoder ever touches the compressed data. Read them out of the dump and give the real width. The first eight bytes are highlighted, hovering any byte prints its offset and ASCII character, and the signature reference under the dump names each chunk.
What you'll learn
- Locate a chunk inside a PNG by its four-letter ASCII type
- Read a 4-byte big-endian integer out of a hex dump
- Explain why file size on disk says nothing about decoded size
- Recognise a decompression bomb as a resource attack, not a malformed file
- Separate metadata a tool wrote from structure a decoder acts on
Skills tested
Prerequisites
- Hexadecimal to decimal conversion
- What a file header is
How it works
A PNG is a signature followed by a run of chunks, and every chunk is the same shape: a 4-byte length, a 4-byte ASCII type, the data, then a 4-byte checksum. That regularity is what lets you navigate a dump without a parser. Find the four letters that name a chunk and you know exactly where its data starts.
The first chunk is always IHDR, and its first eight bytes are the width and the height, each a 32-bit big-endian integer. Big-endian means the most significant byte is written first, so 00 00 75 30 is 0x7530 and not 0x3075. Every decoder reads those eight bytes before anything else, because it has to allocate the output buffer before it can decompress into it.
That allocation is the attack. Compressed size and decoded size are unrelated: a single flat colour across an enormous canvas compresses to a few kilobytes and expands to gigabytes. Any service that accepts an image, checks that the upload is small, and then hands it to a library has already lost, because the library sizes its buffer from the header and the header is attacker-controlled.
Common mistakes
- Answering 64. The
64x64visible in the ASCII gutter lives in atEXtcomment chunk. It is a note, and the upload log copied it without looking at the header. - Answering 20000. That is the height. Width comes first in
IHDR. - Reading the bytes little-endian. PNG integers are big-endian, so
00 00 75 30is 30000, not 807534592. - Starting to count at offset 8. Offsets 8 to 11 are the chunk length and 12 to 15 are the type. The width begins at offset 16.
- Reading the pHYs chunk. It holds pixels per metre, which is resolution, not size.
How to defend against it
Decide whether to decode from the header, before you decode.
- Parse
IHDRyourself and reject anything above a pixel budget. Total pixels matters more than either dimension: 30000 by 20000 is 600 megapixels, and a 50 megapixel cap would have stopped this at the door. - Set the limits your image library already offers. Most of them expose a maximum pixel count and refuse oversized files if you turn it on.
- Do the work in a process with a hard memory limit so one bad file kills one worker instead of the pool, and never in the request thread.
- Treat every text chunk as untrusted decoration. Dimensions recorded at upload time should come from your own parse, not from a comment in the file.