wagtail / wagtail

A Django content management system focused on flexibility and user experience
https://wagtail.org
BSD 3-Clause "New" or "Revised" License
18.04k stars 3.8k forks source link

If CSP blocks data: URLs, uploading images with EXIF thumbnails triggers an error and the thumbnail is not displayed #12368

Open mgax opened 5 days ago

mgax commented 5 days ago

Issue Summary

With a Content Security Policy that blocks data: URLs (because the MDN warns against allowing them), when an editor uploads an image with a thumbnail embedded in the EXIF, the image uploader JS code tries to load a thumbnail image with a data: URL. On the other hand, if the image does not already contain a thumbnail, the JS code generates one, and loads it using a blob: URL.

Steps to Reproduce

  1. Set up the bakerydemo project with the following settings in local.py:
    from .base import MIDDLEWARE
    MIDDLEWARE.append("csp.middleware.CSPMiddleware")
    CSP_REPORT_ONLY = False
    CSP_DEFAULT_SRC = ["'self'"]
    CSP_CONNECT_SRC = ["'self'", "releases.wagtail.org"]
    CSP_FRAME_SRC = ["'self'"]
    CSP_IMG_SRC = ["'self'", "blob:"]
    CSP_OBJECT_SRC = ["'none'"]
    CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "'unsafe-eval'"]
    CSP_STYLE_SRC = ["'self'", "'unsafe-inline'"]
  2. Go to http://localhost:8000/admin/images/multiple/add/
  3. Upload this image iguana-orig

The following message appears in the web console (example from Firefox):

Content-Security-Policy: The page’s settings blocked the loading of a resource (img-src) at data:image/jpeg,%ff%d8%ff%e0%00%10%4a%46… because it violates the following directive: “img-src 'self' blob:” load-image.min.js:1:373

The upload itself still works, it's just the thumbnail that is broken.

This other image, with EXIF stripped (exiftool -all= iguana-noexif.jpg), does not trigger an error:

iguana-noexif

Technical details

Working on this

Anyone can contribute to this. View our contributing guidelines, add a comment to the issue once you’re ready to start.

thibaudcolas commented 5 days ago

Thank you @mgax, I can reproduce this with your instructions. Do you have thoughts on what we should do about this? I can think of a few options but I have no notion of how desirable they are:

  1. Keep the upload preview logic as-is and just style the preview thumbnail so there is a better-looking placeholder when the actual image fails to load
  2. Switch to blob: URLs always?
  3. Or <canvas>?
  4. Or Wagtail image renditions?

For now I’ll add this to #7053.

mgax commented 5 days ago

Do you have thoughts on what we should do about this?

I think the issue stems from this if: https://github.com/wagtail/wagtail/blob/abcb2da37259c16d1efd16d7233794f00bb29680/wagtail/images/static_src/wagtailimages/js/vendor/jquery.fileupload-image.js#L202-L203

When there is a thumbnail in the EXIF, the thumbnail variable contains a value in the form data:image/jpeg,..., which is then injected into the DOM. But that's vendorized code, from jQuery-File-Upload, which is not maintained any more. And data.exif comes from load-image.min.js, which was likely copied from JavaScript-Load-Image, which hasn't had an update in 3 years (and our version is from 10 years ago when it was first added to the repository).

I think we should replace it with a modern, maintained alternative. And in the mean time, we could convert that thumbnail value on the fly, from a data: URL to a blob: URL. ChatGPT helped me write this little monster:

--- a/wagtail/images/static_src/wagtailimages/js/vendor/jquery.fileupload-image.js
+++ b/wagtail/images/static_src/wagtailimages/js/vendor/jquery.fileupload-image.js
@@ -200,6 +200,8 @@
                     }
                     if (options.thumbnail) {
                         thumbnail = data.exif.get('Thumbnail');
+                        const [type, encodedData] = thumbnail.split(":")[1].split(",");
+                        thumbnail = URL.createObjectURL(new Blob([new Uint8Array((str => str.replace(/%([0-9A-F]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).split('').map(c => c.charCodeAt(0)))(encodedData))], { type }));
                         if (thumbnail) {
                             loadImage(thumbnail, resolve, options);
                             return dfd.promise();
@@ -312,4 +314,4 @@

     });