Repository: saleor/saleor

Issue:
Bug: Update image validation - allow only formats that are supported by thumbnails
### What are you trying to achieve?

The error should be raised for uploading an image that is not supported by thumbnails.

### Steps to reproduce the problem

1. Upload the image for a field that supports thumbnails (for example user avatar), use the image extension that is not listed [here](https://github.com/saleor/saleor/blob/70549a473061fe308c66962bd9da64d3979e0788/saleor/thumbnail/__init__.py#L18-L34).
2. Try to fetch the thumbnails for the uploaded file.

### What did you expect to happen?

The `ValidationError` should be raised when an invalid file is uploaded.

### Logs

_No response_

### Environment

Saleor version:3.6+



Discussion:

Resolving patch:
diff --git a/saleor/graphql/core/utils/__init__.py b/saleor/graphql/core/utils/__init__.py
--- a/saleor/graphql/core/utils/__init__.py
+++ b/saleor/graphql/core/utils/__init__.py
@@ -14,6 +14,7 @@
 
 from ....core.utils import generate_unique_slug
 from ....plugins.webhook.utils import APP_ID_PREFIX
+from ....thumbnail import MIME_TYPE_TO_PIL_IDENTIFIER
 
 if TYPE_CHECKING:
     from django.db.models import Model
@@ -50,6 +51,13 @@ def is_image_mimetype(mimetype: str) -> bool:
     return mimetype.startswith("image/")
 
 
+def is_supported_image_mimetype(mimetype: str) -> bool:
+    """Check if mimetype is a mimetype that thumbnails support."""
+    if mimetype is None:
+        return False
+    return mimetype in MIME_TYPE_TO_PIL_IDENTIFIER.keys()
+
+
 def is_image_url(url: str) -> bool:
     """Check if file URL seems to be an image."""
     if url.endswith(".webp"):
@@ -68,7 +76,7 @@ def validate_image_url(url: str, field_name: str, error_code: str) -> None:
     head = requests.head(url)
     header = head.headers
     content_type = header.get("content-type")
-    if content_type is None or not is_image_mimetype(content_type):
+    if content_type is None or not is_supported_image_mimetype(content_type):
         raise ValidationError(
             {field_name: ValidationError("Invalid file type.", code=error_code)}
         )
@@ -83,7 +91,7 @@ def get_filename_from_url(url: str) -> str:
 
 
 def validate_image_file(file, field_name, error_class) -> None:
-    """Validate if the file is an image."""
+    """Validate if the file is an image supported by thumbnails."""
     if not file:
         raise ValidationError(
             {
@@ -92,7 +100,7 @@ def validate_image_file(file, field_name, error_class) -> None:
                 )
             }
         )
-    if not is_image_mimetype(file.content_type):
+    if not is_supported_image_mimetype(file.content_type):
         raise ValidationError(
             {
                 field_name: ValidationError(
@@ -105,7 +113,7 @@ def validate_image_file(file, field_name, error_class) -> None:
 
 def _validate_image_format(file, field_name, error_class):
     """Validate image file format."""
-    allowed_extensions = [ext.lower() for ext in Image.EXTENSION]
+    allowed_extensions = get_allowed_extensions()
     _file_name, format = os.path.splitext(file._name)
     if not format:
         raise ValidationError(
@@ -126,6 +134,15 @@ def _validate_image_format(file, field_name, error_class):
         )
 
 
+def get_allowed_extensions():
+    """Return image extension lists that are supported by thumbnails."""
+    return [
+        ext.lower()
+        for ext, file_type in Image.EXTENSION.items()
+        if file_type.upper() in MIME_TYPE_TO_PIL_IDENTIFIER.values()
+    ]
+
+
 def validate_slug_and_generate_if_needed(
     instance: "Model",
     slugable_field: str,
diff --git a/saleor/thumbnail/__init__.py b/saleor/thumbnail/__init__.py
--- a/saleor/thumbnail/__init__.py
+++ b/saleor/thumbnail/__init__.py
@@ -18,12 +18,11 @@ class ThumbnailFormat:
 MIME_TYPE_TO_PIL_IDENTIFIER = {
     "image/bmp": "BMP",
     "image/dcx": "DCX",
-    "image/eps": "eps",
+    "image/eps": "EPS",
     "image/gif": "GIF",
     "image/jpeg": "JPEG",
     "image/pcd": "PCD",
     "image/pcx": "PCX",
-    "application/pdf": "PDF",
     "image/png": "PNG",
     "image/x-ppm": "PPM",
     "image/psd": "PSD",


Test patch:
diff --git a/saleor/graphql/core/tests/test_core.py b/saleor/graphql/core/tests/test_core.py
--- a/saleor/graphql/core/tests/test_core.py
+++ b/saleor/graphql/core/tests/test_core.py
@@ -30,6 +30,7 @@
     get_duplicated_values,
     get_filename_from_url,
     is_image_mimetype,
+    is_supported_image_mimetype,
     snake_to_camel_case,
     validate_image_file,
     validate_image_url,
@@ -320,6 +321,25 @@ def test_validate_image_file_invalid_file_extension():
     )
 
 
+def test_validate_image_file_file_extension_not_supported_by_thumbnails():
+    # given
+    img_data = BytesIO()
+    image = Image.new("RGB", size=(1, 1))
+    image.save(img_data, format="JPEG")
+    img = SimpleUploadedFile("product.pxr", img_data.getvalue(), "image/jpeg")
+    field = "image"
+
+    # when
+    with pytest.raises(ValidationError) as exc:
+        validate_image_file(img, field, ProductErrorCode)
+
+    # then
+    assert (
+        exc.value.args[0][field].message
+        == "Invalid file extension. Image file required."
+    )
+
+
 def test_get_filename_from_url_unique():
     # given
     file_format = "jpg"
@@ -357,6 +377,28 @@ def test_is_image_mimetype_invalid_mimetype():
     assert not result
 
 
+def test_is_supported_image_mimetype_valid_mimetype():
+    # given
+    valid_mimetype = "image/jpeg"
+
+    # when
+    result = is_supported_image_mimetype(valid_mimetype)
+
+    # then
+    assert result
+
+
+def test_is_supported_image_mimetype_invalid_mimetype():
+    # given
+    invalid_mimetype = "application/javascript"
+
+    # when
+    result = is_supported_image_mimetype(invalid_mimetype)
+
+    # then
+    assert not result
+
+
 def test_validate_image_url_valid_image_response(monkeypatch):
     # given
     valid_image_response_mock = Mock()
diff --git a/saleor/graphql/product/tests/mutations/test_product_media_create.py b/saleor/graphql/product/tests/mutations/test_product_media_create.py
--- a/saleor/graphql/product/tests/mutations/test_product_media_create.py
+++ b/saleor/graphql/product/tests/mutations/test_product_media_create.py
@@ -8,7 +8,7 @@
 from .....graphql.tests.utils import get_graphql_content, get_multipart_request_body
 from .....product import ProductMediaTypes
 from .....product.error_codes import ProductErrorCode
-from .....product.tests.utils import create_image, create_pdf_file_with_image_ext
+from .....product.tests.utils import create_image, create_zip_file_with_image_ext
 
 PRODUCT_MEDIA_CREATE_QUERY = """
     mutation createProductMedia(
@@ -216,7 +216,7 @@ def test_invalid_product_media_create_mutation(
         }
     }
     """
-    image_file, image_name = create_pdf_file_with_image_ext()
+    image_file, image_name = create_zip_file_with_image_ext()
     variables = {
         "product": graphene.Node.to_global_id("Product", product.id),
         "image": image_name,
diff --git a/saleor/graphql/product/tests/test_category.py b/saleor/graphql/product/tests/test_category.py
--- a/saleor/graphql/product/tests/test_category.py
+++ b/saleor/graphql/product/tests/test_category.py
@@ -13,7 +13,7 @@
 from ....core.utils.json_serializer import CustomJsonEncoder
 from ....product.error_codes import ProductErrorCode
 from ....product.models import Category, Product, ProductChannelListing
-from ....product.tests.utils import create_image, create_pdf_file_with_image_ext
+from ....product.tests.utils import create_image, create_zip_file_with_image_ext
 from ....product.utils.costs import get_product_costs_data
 from ....tests.utils import dummy_editorjs
 from ....thumbnail.models import Thumbnail
@@ -828,7 +828,7 @@ def test_category_update_mutation_invalid_background_image(
     media_root,
 ):
     # given
-    image_file, image_name = create_pdf_file_with_image_ext()
+    image_file, image_name = create_zip_file_with_image_ext()
     image_alt = "Alt text for an image."
     size = 128
     thumbnail_mock = MagicMock(spec=File)
diff --git a/saleor/graphql/product/tests/test_collection.py b/saleor/graphql/product/tests/test_collection.py
--- a/saleor/graphql/product/tests/test_collection.py
+++ b/saleor/graphql/product/tests/test_collection.py
@@ -10,7 +10,7 @@
 
 from ....product.error_codes import CollectionErrorCode, ProductErrorCode
 from ....product.models import Collection, CollectionChannelListing, Product
-from ....product.tests.utils import create_image, create_pdf_file_with_image_ext
+from ....product.tests.utils import create_image, create_zip_file_with_image_ext
 from ....tests.utils import dummy_editorjs
 from ....thumbnail.models import Thumbnail
 from ...core.enums import ThumbnailFormatEnum
@@ -798,7 +798,7 @@ def test_update_collection_invalid_background_image(
     media_root,
 ):
     # given
-    image_file, image_name = create_pdf_file_with_image_ext()
+    image_file, image_name = create_zip_file_with_image_ext()
     image_alt = "Alt text for an image."
 
     size = 128
diff --git a/saleor/product/tests/utils.py b/saleor/product/tests/utils.py
--- a/saleor/product/tests/utils.py
+++ b/saleor/product/tests/utils.py
@@ -12,7 +12,7 @@ def create_image(image_name="product2"):
     return image, image_name
 
 
-def create_pdf_file_with_image_ext():
+def create_zip_file_with_image_ext():
     file_name = "product.jpg"
-    file_data = SimpleUploadedFile(file_name, b"product_data", "application/pdf")
+    file_data = SimpleUploadedFile(file_name, b"product_data", "application/zip")
     return file_data, file_name