Repository: mdn/kuma

Issue:
Magic is incorrectly using image/svg as MIME type for SVG files, preventing uploads of SVG files
Magic or libmagic is incorrectly using `image/svg` as the MIME type for SVG uploads, rather than `image/svg+xml`, which is the only correct MIME type [1] (and the one we've always used in the past).

Normally, we keep SVG uploads disabled for security reasons since SVG can contain JavaScript code. However, we like to use it for diagrams and the like because they can be translated, and they are more efficient for that purpose in many cases. When we want to upload SVG, we go to the admin panel in Django and in the Constance panel, add "image/svg+xml" to the `WIKI_ATTACHMENT_ALLOWED_TYPES` string. Then we upload the file and restore the previous string to that preference.

This is no longer working; even with `image/svg+xml` listed in the preference, uploads fail with an error announcing that "Files of this type are not permitted." This should work, however.

This issue is blocking work on WebXR and potentially other documentation that requires the use of diagrams to explain things properly. Hopefully the fix is not complicated.

[1] https://www.iana.org/assignments/media-types/media-types.xhtml#image


Discussion:
Hey Sheppy, can you try image/svg instead? Looks like it's an issue in a library we are using.
Using `image/svg` allows uploading but then the resulting file is not recognized by CKEditor or web browsers as a valid image file and will not render. The library needs to be fixed.
@atopal ^
How about this for a solution:

1) If the computed mimetype is `image/svg` we override and say it's `image/svg+xml`.
2) If you're a superuser or staff and the mimetype is `image/svg+xml` we skip the validation check and just allow it. 

That worked for me in a quick hack. Now I was able to insert my SVG:
<img width="907" alt="Screen Shot 2019-11-20 at 2 51 41 PM" src="https://user-images.githubusercontent.com/26739/69272989-94ab8700-0ba5-11ea-8a5f-e0e09c35794f.png">

It's a bit convoluted but it certainly fixes the problem. 

@escattone You looked into this but I see no comment from you. Perhaps you wrote your comment elsewhere. 


That works for me. It will be better than the current situation by far.
[There are 29 users with staff status](https://developer.mozilla.org/admin/users/user/?is_staff__exact=1). 
If we go for this solution, I wouldn't mind removing the use of Constance and just switching to a plain environment variable for this. 
@escattone If you don't mind it, I can take a stab. 
@peterbe I had spent some time last week looking into getting `libmagic` to return `image/svg+xml` rather than `image/svg`, but I couldn't see how to resolve that. It seems like a configuration bug, but I'm not sure. I like your proposed workaround, so please go ahead and take a stab, thanks!
Peter, if you are working on this, do we need to remove something from the current sprint to accommodate that?
> Peter, if you are working on this, do we need to remove something from the current sprint to accommodate that?

I'm ahead on my sprint stuff. Besides, this is easy to do. 

Resolving patch:
diff --git a/kuma/attachments/forms.py b/kuma/attachments/forms.py
--- a/kuma/attachments/forms.py
+++ b/kuma/attachments/forms.py
@@ -24,6 +24,7 @@ class Meta:
         fields = ('file', 'title', 'description', 'comment')
 
     def __init__(self, *args, **kwargs):
+        self.allow_svg_uploads = kwargs.pop('allow_svg_uploads', False)
         super(AttachmentRevisionForm, self).__init__(*args, **kwargs)
         self.mime_type = None
 
@@ -39,11 +40,21 @@ def clean(self):
         nulls = EMPTY_VALUES + (AttachmentRevision.DEFAULT_MIME_TYPE,)
         submitted_mime_type = cleaned_data.get('mime_type')
 
-        if (submitted_mime_type in nulls) and ('file' in cleaned_data):
+        if submitted_mime_type in nulls and 'file' in cleaned_data:
             self.mime_type = self.mime_type_from_file(cleaned_data['file'])
-            allowed_mime_types = config.WIKI_ATTACHMENT_ALLOWED_TYPES.split()
-            if self.mime_type not in allowed_mime_types:
-                raise forms.ValidationError(MIME_TYPE_INVALID, code='invalid')
+            if self.mime_type.startswith('image/svg') and self.allow_svg_uploads:
+                # The `magic.Magic()` will, for unknown reasons, sometimes
+                # think an SVG image's mime type is `image/svg` which not
+                # a valid mime type actually.
+                # See https://www.iana.org/assignments/media-types/media-types.xhtml#image
+                # So correct that.
+                if self.mime_type == 'image/svg':
+                    self.mime_type = 'image/svg+xml'
+            else:
+                allowed_mime_types = config.WIKI_ATTACHMENT_ALLOWED_TYPES.split()
+                if self.mime_type not in allowed_mime_types:
+                    raise forms.ValidationError(
+                        MIME_TYPE_INVALID, code='invalid')
 
         return cleaned_data
 
diff --git a/kuma/attachments/views.py b/kuma/attachments/views.py
--- a/kuma/attachments/views.py
+++ b/kuma/attachments/views.py
@@ -84,7 +84,13 @@ def edit_attachment(request, document_slug, document_locale):
     if not allow_add_attachment_by(request.user):
         raise PermissionDenied
 
-    form = AttachmentRevisionForm(data=request.POST, files=request.FILES)
+    form = AttachmentRevisionForm(
+        data=request.POST,
+        files=request.FILES,
+        # Only staff users are allowed to upload SVG files because SVG files
+        # can contain embedded inline scripts.
+        allow_svg_uploads=request.user.is_staff
+    )
     if form.is_valid():
         revision = form.save(commit=False)
         revision.creator = request.user


Test patch:
diff --git a/kuma/attachments/tests/test_views.py b/kuma/attachments/tests/test_views.py
--- a/kuma/attachments/tests/test_views.py
+++ b/kuma/attachments/tests/test_views.py
@@ -125,6 +125,47 @@ def test_mime_type_filtering(self):
         self.assertContains(response, 'Files of this type are not permitted.')
         _file.close()
 
+    def test_svg_mime_type_staff_override(self):
+        """
+        Staff users are allowed to upload SVG images. Only.
+        """
+        _file = make_test_file(
+            content='<svg viewBox="0 0 841.9 595.3"></svg>', suffix='.svg')
+        post_data = {
+            'title': 'Test Svg upload',
+            'description': 'Mime type only allowed for some users.',
+            'comment': 'Initial upload',
+            'file': _file,
+        }
+        # Remember, self.client use logged in as user 'admin'
+        response = self.client.post(self.files_url, data=post_data,
+                                    HTTP_HOST=settings.WIKI_HOST)
+        assert response.status_code == 302  # means it worked
+        assert_no_cache_header(response)
+        _file.close()
+        attachment_revision, = AttachmentRevision.objects.all()
+        assert attachment_revision.mime_type == 'image/svg+xml'
+
+    def test_svg_mime_type_non_staff(self):
+        """
+        Regular users are not allowed to upload SVG images.
+        """
+        _file = make_test_file(
+            content='<svg viewBox="0 0 841.9 595.3"></svg>', suffix='.svg')
+        post_data = {
+            'title': 'Test Svg upload',
+            'description': 'Mime type only allowed for some users.',
+            'comment': 'Initial upload',
+            'file': _file,
+        }
+        self.client.login(username='testuser', password='testpass')
+        response = self.client.post(self.files_url, data=post_data,
+                                    HTTP_HOST=settings.WIKI_HOST)
+        assert response.status_code == 200  # means it didn't upload
+        assert_no_cache_header(response)
+        self.assertContains(response, 'Files of this type are not permitted.')
+        _file.close()
+
     def test_intermediate(self):
         """
         Test that the intermediate DocumentAttachment gets created