"""Image sanitization for user-uploaded photos. Re-encodes inbound images through Pillow to: - strip EXIF (GPS lat/lon, device fingerprint, owner metadata) - normalize byte stream (a polyglot JPEG with appended payload survives MIME check but is dropped on re-encode — only the actual image survives) - enforce max dimension (defend against pixel-flood DoS in downstream renders) - produce uniform JPEG output (smaller storage; no transparency support needed for apartment photos) Closes finding #6 from 2026-05-24 trade-in audit. """ from __future__ import annotations import io import logging from PIL import Image, UnidentifiedImageError logger = logging.getLogger(__name__) # Max longest edge after re-encode. 2400px = enough for retina apt photo, # small enough to bound storage & decode cost. _MAX_DIMENSION = 2400 # JPEG quality. 85 is industry standard for photos — visually lossless, # ~70% size reduction vs original phone JPEGs. _JPEG_QUALITY = 85 # Output content type after sanitization (all inputs transcoded to JPEG). SANITIZED_CONTENT_TYPE = "image/jpeg" class ImageSanitizationError(ValueError): """Raised when inbound bytes cannot be decoded as an image.""" def sanitize_image(content: bytes) -> tuple[bytes, str]: """Decode arbitrary image bytes and re-encode as clean JPEG. Args: content: raw uploaded bytes from the client. Returns: (sanitized_bytes, "image/jpeg") — both ready for storage/serving. Raises: ImageSanitizationError: bytes are not a recognized image format. """ try: with Image.open(io.BytesIO(content)) as img: img.load() # force full decode; surfaces UnidentifiedImageError early # Convert any mode (RGBA, P, CMYK, ...) to RGB for JPEG output. if img.mode != "RGB": img = img.convert("RGB") # Resize if oversized; preserve aspect ratio. if max(img.size) > _MAX_DIMENSION: img.thumbnail((_MAX_DIMENSION, _MAX_DIMENSION), Image.Resampling.LANCZOS) buf = io.BytesIO() # save() does NOT carry over .info / EXIF unless we pass exif=...; we don't. img.save(buf, format="JPEG", quality=_JPEG_QUALITY, optimize=True) return buf.getvalue(), SANITIZED_CONTENT_TYPE except UnidentifiedImageError as e: raise ImageSanitizationError("not a recognized image format") from e except Exception as e: # PIL can raise broad errors (DecompressionBombError, OSError on truncated, ...); # normalize to one user-facing error type. logger.warning("image sanitization failed: %s", e) raise ImageSanitizationError(f"image decode failed: {e}") from e