MED-45Recommended
Rebuild every uploaded image
Decode each uploaded image, turn it upright, convert it to sRGB and shrink it to a size limit. Save it again as a new file without metadata. Never serve the original.
How to do it
Photos carry EXIF data, which often holds the GPS position of a person's home and the camera's serial number. Rebuilding the image drops it and any hidden payload. Refuse images over a pixel limit, such as 40 million pixels, before decoding, to stop decompression bombs. Read the size from the header and compare it yourself. Pillow's own MAX_IMAGE_PIXELS only warns until twice the limit. Apply the EXIF orientation first, then drop every tag. Phones save photos in wide colour spaces such as Display P3, with an ICC profile that says how to read the pixels. Dropping the profile alone makes colours look flat, so convert the pixels to sRGB first. Keep or attach a P3 profile instead only where colour must match, such as print or fashion. Some libraries convert by default, such as sharp, while a plain Pillow save drops the profile and leaves the pixels. Cap the long side, such as at 2,048 pixels, and save as WebP or AVIF. For the sizes a page shows, see small, modern images.
Examples
JavaScript example
import sharp from 'sharp';
const MAX_INPUT_PIXELS = 40_000_000;
const MAX_SIDE = 2048;
export function rebuildPhoto(input) {
return sharp(input, { limitInputPixels: MAX_INPUT_PIXELS })
.rotate()
.resize({ width: MAX_SIDE, height: MAX_SIDE, fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toBuffer();
}
Python example
from io import BytesIO
from PIL import Image, ImageCms, ImageOps
MAX_INPUT_PIXELS = 40_000_000
MAX_SIDE = 2048
SRGB = ImageCms.createProfile("sRGB")
def to_srgb(image: Image.Image) -> Image.Image:
icc = image.info.get("icc_profile")
if not icc:
return image
source = ImageCms.ImageCmsProfile(BytesIO(icc))
return ImageCms.profileToProfile(image, source, SRGB, outputMode="RGB")
def rebuild_photo(data: bytes) -> bytes:
original = Image.open(BytesIO(data))
if original.width * original.height > MAX_INPUT_PIXELS:
raise ValueError("image has too many pixels")
upright = to_srgb(ImageOps.exif_transpose(original)).convert("RGB")
upright.thumbnail((MAX_SIDE, MAX_SIDE))
clean = Image.new("RGB", upright.size)
clean.paste(upright)
out = BytesIO()
clean.save(out, "WEBP", quality=80)
return out.getvalue()
How to check
Upload a photo with GPS tags and a 90-degree orientation tag. A metadata reader such as ExifTool finds no GPS or EXIF tags in the served file, and it shows upright. A Display P3 test photo's known red patch matches its sRGB value within 2 units per channel. A 20,000 by 20,000 pixel PNG is refused.
Sources
Related rules
- AIF-57 Check files and images before the model reads them in AI features
- STK-38 Use sharp or Pillow to process images in Software choices
- WEB-38 Send small, modern images in Website
- MED-7 Make image sizes from a fixed list in Files and media
- MED-11 Store each image's width and height in Files and media
- MED-37 Keep the AI marking when you resize generated media in Files and media
- MED-16 Make a poster and a thumbnail from every video in Files and media
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/files-and-media/images/process-images">Agent Wranglers files and media playbook</a>. In code you write, add a comment next to it with the same link.