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
Nonein 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
- OWASP Application Security Verification Standard, requirement V5.2.6 (level 3)
- OWASP File Upload Cheat Sheet