From baa597f19bd4a7276cd4cbfba0233f3a1d451625 Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Fri, 16 Oct 2026 15:48:31 +0200
Subject: [PATCH v14 03/12] Reject images with wrong size before loading them
 fully

The old code was fully materializing the image in memory before checking
the size constraints, so a PNG "fork bomb" could still use a lot of
bitmap memory even when it would be rejected at the end. Check the
header first, and only then load the image.
---
 pgweb/util/fields.py | 52 ++++++++++++++++++++++++++------------------
 1 file changed, 31 insertions(+), 21 deletions(-)

diff --git a/pgweb/util/fields.py b/pgweb/util/fields.py
index baf4bed6..da99b5e7 100644
--- a/pgweb/util/fields.py
+++ b/pgweb/util/fields.py
@@ -5,7 +5,7 @@ from .forms import ImageBinaryFormField
 
 import io
 
-from PIL import ImageFile
+from PIL import Image
 
 from pgweb.util.image import rescale_image, apply_exif_orientation, EXIF_ORIENTATION_TAG
 
@@ -49,32 +49,42 @@ class ImageBinaryField(models.Field):
             v = bytes(value)
         else:
             v = value
+
         try:
-            p = ImageFile.Parser()
-            p.feed(v)
-            p.close()
-            img = p.image
+            # Opening parses only the header, so the format and dimensions can
+            # be checked before any pixel data is decoded.
+            img = Image.open(io.BytesIO(v))
         except Exception as e:
-            raise ValidationError("Could not parse image: %s" % e)
+            raise ValidationError("Could not parse image")
 
         if img.format.upper() not in ('JPEG', 'PNG'):
             raise ValidationError("Only JPEG or PNG files are allowed")
 
-        # Bake EXIF orientation in; re-encode only when actually rotated so
-        # untouched JPEGs are not needlessly recompressed.
-        if img.getexif().get(EXIF_ORIENTATION_TAG, 1) != 1:
-            fmt = img.format
-            img = apply_exif_orientation(img)
-            saver = io.BytesIO()
-            img.save(saver, format=fmt)
-            value = saver.getvalue()
-
-        if self.resolution:
-            if img.size[0] != self.resolution[0] or img.size[1] != self.resolution[1]:
-                if self.auto_scale:
-                    value = rescale_image(img, self.resolution, centered=True)
-                else:
-                    raise ValidationError("Image size must be {}x{}".format(*self.resolution))
+        if self.resolution and not self.auto_scale and img.size != tuple(self.resolution):
+            # Reject on the header dimensions, so an oversized (or
+            # decompression-bomb) upload is rejected without decoding its
+            # pixels.
+            raise ValidationError("Image size must be {}x{}".format(*self.resolution))
+
+        try:
+            # Load the pixels. Only images that will be kept (possibly
+            # rescaled) reach this point, so the decode below is bounded.
+            img.load()
+
+            # Bake EXIF orientation in; re-encode only when actually rotated so
+            # untouched JPEGs are not needlessly recompressed.
+            if img.getexif().get(EXIF_ORIENTATION_TAG, 1) != 1:
+                fmt = img.format
+                img = apply_exif_orientation(img)
+                saver = io.BytesIO()
+                img.save(saver, format=fmt)
+                value = saver.getvalue()
+
+            if self.resolution and img.size != tuple(self.resolution):
+                # auto_scale; the not-auto_scale case was rejected above.
+                value = rescale_image(img, self.resolution, centered=True)
+        except Exception as e:
+            raise ValidationError("Could not parse image")
 
         return value
 
-- 
2.53.0

