Repository: saleor/saleor

Issue:
Mutation productImageCreate doesn't validate file extension
The mutation `productImageCreate` which is used to upload product images from the dashboard doesn't validate file extensions. Now it is possible to upload e.g. an audio file.


Discussion:
I think we should be validating MIME types and not just file extensions?
Yes, that would be the more reliable way.

Resolving patch:
diff --git a/saleor/graphql/product/mutations/products.py b/saleor/graphql/product/mutations/products.py
--- a/saleor/graphql/product/mutations/products.py
+++ b/saleor/graphql/product/mutations/products.py
@@ -444,12 +444,20 @@ class Meta:
         description = 'Creates a product image.'
         model = models.ProductImage
 
+    @classmethod
+    def clean_input(cls, info, instance, input, errors):
+        cleaned_input = super().clean_input(info, instance, input, errors)
+        uploaded_image = cleaned_input['image']
+        if not uploaded_image.content_type.startswith('image/'):
+            cls.add_error(errors, 'image', 'Invalid file type')
+        return cleaned_input
+
     @classmethod
     def user_is_allowed(cls, user, input):
         return user.has_perm('product.edit_product')
 
 
-class ProductImageUpdate(ModelMutation):
+class ProductImageUpdate(ProductImageCreate):
     class Arguments:
         id = graphene.ID(
             required=True, description='ID of a product image to update.')
@@ -461,10 +469,6 @@ class Meta:
         description = 'Updates a product image.'
         model = models.ProductImage
 
-    @classmethod
-    def user_is_allowed(cls, user, input):
-        return user.has_perm('product.edit_product')
-
 
 class ProductImageReorder(BaseMutation):
     class Arguments:


Test patch:
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -6,7 +6,8 @@
 from django.utils.text import slugify
 from graphql_relay import to_global_id
 from prices import Money
-from tests.utils import create_image, get_graphql_content
+from tests.utils import (
+    create_image, create_pdf_file_with_image_ext, get_graphql_content)
 
 from saleor.product.models import (
     Category, Collection, Product, ProductAttribute, ProductType)
@@ -829,6 +830,37 @@ def test_product_image_create_mutation(admin_api_client, product):
     assert product.images.first().image.name == file_name
 
 
+def test_invalid_product_image_create_mutation(admin_api_client, product):
+    query = """
+    mutation createProductImage($image: Upload!, $product: ID!) {
+        productImageCreate(input: {image: $image, product: $product}) {
+            productImage {
+                id
+                image
+                url
+                sortOrder
+            }
+            errors {
+                field
+                message
+            }
+        }
+    }
+    """
+    image_file, image_name = create_pdf_file_with_image_ext()
+    variables = {
+        'product': graphene.Node.to_global_id('Product', product.id),
+        'image': image_name}
+    body = get_multipart_request_body(query, variables, image_file, image_name)
+    response = admin_api_client.post_multipart(reverse('api'), body)
+    content = get_graphql_content(response)
+    assert content['data']['productImageCreate']['errors'] == [{
+        'field': 'image',
+        'message': 'Invalid file type'}]
+    product.refresh_from_db()
+    assert product.images.count() == 0
+
+
 def test_product_image_update_mutation(admin_api_client, product_with_image):
     product = product_with_image
     query = """
@@ -855,6 +887,42 @@ def test_product_image_update_mutation(admin_api_client, product_with_image):
     assert data['productImage']['alt'] == alt
 
 
+def test_invalid_product_image_update_mutation(
+        admin_api_client, product_with_image):
+    product = product_with_image
+    query = """
+    mutation updateProductImage($image: Upload!, $alt: String, $product: ID!, $id: ID!) {
+        productImageUpdate(id: $id, input: {image: $image, alt: $alt, product: $product}) {
+            productImage {
+                image
+            }
+            errors {
+                field
+                message
+            }
+        }
+    }
+    """
+    image_obj = product_with_image.images.first()
+    image = image_obj.image
+    new_image_file, new_image_name = create_pdf_file_with_image_ext()
+    variables = {
+        'product': graphene.Node.to_global_id('Product', product.id),
+        'image': new_image_name,
+        'id': graphene.Node.to_global_id('ProductImage', image_obj.id),
+    }
+    body = get_multipart_request_body(
+        query, variables, new_image_file, new_image_name)
+    response = admin_api_client.post_multipart(reverse('api'), body)
+    content = get_graphql_content(response)
+    assert content['data']['productImageUpdate']['errors'] == [{
+        'field': 'image',
+        'message': 'Invalid file type'}]
+    product_with_image.refresh_from_db()
+    assert product_with_image.images.count() == 1
+    assert product_with_image.images.first().image == image
+
+
 def test_product_image_delete(admin_api_client, product_with_image):
     product = product_with_image
     query = """
diff --git a/tests/utils.py b/tests/utils.py
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -52,3 +52,10 @@ def create_image():
     image = SimpleUploadedFile(
         image_name + '.jpg', img_data.getvalue(), 'image/png')
     return image, image_name
+
+
+def create_pdf_file_with_image_ext():
+    file_name = 'product.jpg'
+    file_data = SimpleUploadedFile(
+        file_name, b'product_data', 'application/pdf')
+    return file_data, file_name