You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2024/04/29 12:40:19 UTC

(superset) 01/02: Verify image magic number

This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch geido/fix/dashboard-pdf-download
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 8c646bc9326f70598c5462e2bee8622facd4f828
Author: Diego Pucci <di...@gmail.com>
AuthorDate: Mon Apr 29 15:39:50 2024 +0300

    Verify image magic number
---
 superset/initialization/__init__.py |  2 +-
 superset/views/img_proxy.py         | 95 ++++++++++++++++++++++++-------------
 2 files changed, 63 insertions(+), 34 deletions(-)

diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index af53db2826..318278e5d1 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -315,7 +315,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         appbuilder.add_view_no_menu(TaggedObjectsModelView)
         appbuilder.add_view_no_menu(TagView)
         appbuilder.add_view_no_menu(ReportView)
-        appbuilder.add_view_no_menu(ImgProxyView())
+        appbuilder.add_view_no_menu(ImgProxyView)
 
         #
         # Add links
diff --git a/superset/views/img_proxy.py b/superset/views/img_proxy.py
index 58ade4246a..3d23f8d6d0 100644
--- a/superset/views/img_proxy.py
+++ b/superset/views/img_proxy.py
@@ -1,64 +1,93 @@
-from typing import Any, Dict
-import requests
+from io import BytesIO
+from typing import Any
 from urllib.parse import urlparse
-from flask import g, request, Response
+
+import requests
+from flask import abort, request, Response
 from flask_appbuilder.api import expose
+from flask_wtf.csrf import same_origin
+from PIL import Image
+
 from superset import event_logger
-from superset.utils.core import (
-    get_user_id,
-)
+from superset.superset_typing import FlaskResponse
+from superset.utils.core import get_user_id
+
 from .base import BaseSupersetView
 
+
 class ImgProxyView(BaseSupersetView):
     route_base = "/img_proxy"
 
     @expose("/")
     @event_logger.log_this
-    def img_proxy(self) -> Response:
+    def img_proxy(self) -> FlaskResponse:
         """
         Proxy to an external URL, to overcome CORS restrictions.
         Returns a HTTP response containing the resource fetched from the external URL.
         """
         if not get_user_id():
-            raise Exception("User context not found")
-        
-        url = request.args.get('url')
+            abort(403)
+
+        url = request.args.get("url")
+        allowed_content_types = ["image/"]
 
         if not url:
-            return Response("URL parameter 'url' is missing", status=400)
+            abort(400)
 
         parsed_url = urlparse(url)
-        if parsed_url.scheme not in ['http', 'https']:
-            return Response("Invalid URL scheme", status=400)
-        
+        if parsed_url.scheme not in ["http", "https"]:
+            abort(400)
+
+        if not request.referrer or (
+            request.referrer and not same_origin(request.referrer, request.url_root)
+        ):
+            abort(403)
+
         try:
             response = self.fetch_resource(url)
         except Exception:
-            return Response("Error fetching resource", status=500)
+            abort(500)
+
+        content_type = response.headers.get("content-type", "")
+
+        if not any(
+            content_type.startswith(content_type_prefix)
+            for content_type_prefix in allowed_content_types
+        ):
+            abort(400)
+
+        try:
+            is_valid_image = self.validate_image(response.content)
+            if not is_valid_image:
+                abort(400)
+        except Exception:
+            abort(500)
 
-        return self.build_response(response)
+        headers: dict[str, Any] = {
+            key: value for (key, value) in response.headers.items()
+        }
 
-    def fetch_resource(self, url: str) -> Any:
+        return Response(response.content, response.status_code, headers)
+
+    def validate_image(self, image_content) -> bool:
+        """
+        Load the image from bytes, Pillow will raise an IOError if the file is not an image
+        """
+        try:
+            with Image.open(BytesIO(image_content)) as img:
+                if img.format:
+                    return True
+            return False
+        except OSError as e:
+            raise e
+        except Exception as e:
+            raise e
+
+    def fetch_resource(self, url: str) -> requests.Response:
         """Fetch the resource from the external server and handle errors."""
         try:
             response = requests.get(url)
             response.raise_for_status()
-
             return response
         except requests.RequestException as e:
             raise e
-
-    def build_response(self, response: requests.Response) -> Response:
-        """Build the HTTP response to return based on the fetched resource."""
-        allowed_content_types = ['image/']
-        content_type = response.headers.get('content-type', '')
-        
-        if not any(content_type.startswith(content_type_prefix) for content_type_prefix in allowed_content_types):
-            return Response("Response is not an allowed resource type", status=400)
-
-        headers: Dict[str, Any] = {key: value for (key, value) in response.headers.items()}
-        
-        return Response(response.content, response.status_code, headers)
-        
-        
-