Safeguard
Application Security•

ASVS 5.0 V5.2.6: Reject Oversized Images to Stop Pixel Floods

A tiny PNG can declare a billion pixels. Check image dimensions from the header and refuse anything over your limit before decoding.

Safeguard Security Team
3 min read

ASVS 5.0 V5.2.6 (level 3) requires the application to reject uploaded images with a pixel size larger than the maximum allowed, to prevent pixel flood attacks.

The key idea: file size is not image size. A heavily compressed image can be a few kilobytes and still describe 30,000 by 30,000 pixels. Decoding it needs several gigabytes of memory.

Why it matters

Decoders allocate memory based on the declared dimensions. A pixel flood file passes your upload size cap, then your thumbnail job tries to build a bitmap of 900 million pixels at four bytes each, and the worker dies or the host starts swapping. Because image processing is usually asynchronous, the failure often looks random and repeats every time the job is retried.

The defense is cheap: dimensions are stored in the file header, so you can read them without decoding the pixels.

How to implement it

Set a maximum width, height and total pixel count based on what the product needs, then check them before decoding.

With sharp in Node, which reads metadata cheaply and enforces a limit while decoding:

import sharp from 'sharp';

const MAX_W = 8000, MAX_H = 8000, MAX_PIXELS = 24_000_000;

export async function checkImage(buffer) {
  const meta = await sharp(buffer, { limitInputPixels: MAX_PIXELS }).metadata();
  if (!meta.width || !meta.height) throw new Error('unreadable image');
  if (meta.width > MAX_W || meta.height > MAX_H || meta.width * meta.height > MAX_PIXELS) {
    throw new Error('image dimensions too large');
  }
  return meta;
}

With Pillow in Python, set the decompression bomb guard and treat its warning as an error:

from PIL import Image
import warnings

Image.MAX_IMAGE_PIXELS = 24_000_000
warnings.simplefilter('error', Image.DecompressionBombWarning)

def check(path):
    with Image.open(path) as im:       # reads header only
        w, h = im.size
        if w > 8000 or h > 8000:
            raise ValueError('too large')
        im.verify()

Also consider animated formats, where frame count multiplies the cost, and multi page TIFF or PDF. Run image processing in a separate worker with memory and time limits, so a bad file cannot take down the web tier.

How to test it

Make a hostile image and see what happens.

from PIL import Image
Image.MAX_IMAGE_PIXELS = None
Image.new('1', (40000, 40000)).save('flood.png')   # tiny file, huge dimensions

Check that flood.png is only a few kilobytes, then upload it. The service should return a 4xx at once, with no spike in memory. Try one just under the limit and confirm it works. Test an animated GIF with many frames and a huge canvas. Watch worker memory during all of these, and confirm a rejected file leaves no job stuck in a retry loop.

Common mistakes

  • Only limiting upload bytes, which a pixel flood passes easily.
  • Decoding first to find out the size.
  • Setting the pixel limit to None in a library to avoid a warning.
  • Ignoring animated and multi page formats.
  • Resizing in the web process with no memory limit.

Quick check

How can you see the dimensions of an image without decoding it?

Read the metadata in the file header, which most image libraries can do cheaply, and compare to your maximum before any decoding.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.