CVE-2024-1728 (published 2024-04-10T17:15Z)

gradio-app/gradio is vulnerable to a local file inclusion vulnerability due to improper validation of user-supplied input in the UploadButton component. Attackers can exploit this vulnerability to read arbitrary files on the filesystem, such as private SSH keys, by manipulating the file path in the request to the `/queue/join` endpoint. This issue could potentially lead to remote code execution. The vulnerability is present in the handling of file upload paths, allowing attackers to redirect file uploads to unintended locations on the server.


Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/gradio-app/gradio
commit_details: repo_url: https://github.com/gradio-app/gradio
author: aliabid94
author_date: 2024-02-21 20:21:20-06:00
msg: Prevent components from working with non-uploaded files (#7465)

* changes

* chanegs

* add changeset

* changes

* changes

* add changeset

* changes

* changes

* changes

---------

Co-authored-by: Ali Abid <aliabid94@gmail.com>
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
repository: repo_url: https://github.com/gradio-app/gradio
repo_name: gradio-app/gradio
description: Build and share delightful machine learning apps, all in Python. 🌟 Star to support our work!
repo_language: Python
owner: gradio-app
file_changes: filename: moody-impalas-rule.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,5 @@
+---
+"gradio": patch
+---
+
+feat:Prevent components from working with non-uploaded files

programming_language: Markdown
filename: blocks.py
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,6 @@ import random
 import secrets
 import string
 import sys
-import tempfile
 import threading
 import time
 import warnings
@@ -70,6 +69,7 @@ from gradio.utils import (
     get_cancel_function,
     get_continuous_fn,
     get_package_version,
+    get_upload_folder,
 )
 
 try:
@@ -119,12 +119,7 @@ class Block:
         self._constructor_args: list[dict]
         self.state_session_capacity = 10000
         self.temp_files: set[str] = set()
-        self.GRADIO_CACHE = str(
-            Path(
-                os.environ.get("GRADIO_TEMP_DIR")
-                or str(Path(tempfile.gettempdir()) / "gradio")
-            ).resolve()
-        )
+        self.GRADIO_CACHE = get_upload_folder()
 
         if render:
             self.render()
@@ -1110,6 +1105,7 @@ class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta):
             inputs=processed_inputs,
             request=None,
             state={},
+            explicit_call=True,
         )
         outputs = outputs["data"]
 
@@ -1299,7 +1295,11 @@ Received inputs:
             )
 
     def preprocess_data(
-        self, fn_index: int, inputs: list[Any], state: SessionState | None
+        self,
+        fn_index: int,
+        inputs: list[Any],
+        state: SessionState | None,
+        explicit_call: bool = False,
     ):
         state = state or SessionState(self)
         block_fn = self.fns[fn_index]
@@ -1326,7 +1326,10 @@ Received inputs:
                     if input_id in state:
                         block = state[input_id]
                     inputs_cached = processing_utils.move_files_to_cache(
-                        inputs[i], block, add_urls=True
+                        inputs[i],
+                        block,
+                        add_urls=True,
+                        check_in_upload_folder=not explicit_call,
                     )
                     if getattr(block, "data_model", None) and inputs_cached is not None:
                         if issubclass(block.data_model, GradioModel):  # type: ignore
@@ -1522,8 +1525,14 @@ Received outputs:
 
         return data
 
-    def run_fn_batch(self, fn, batch, fn_index, state):
-        return [fn(fn_index, list(i), state) for i in zip(*batch)]
+    def run_fn_batch(self, fn, batch, fn_index, state, explicit_call=None):
+        output = []
+        for i in zip(*batch):
+            args = [fn_index, list(i), state]
+            if explicit_call is not None:
+                args.append(explicit_call)
+            output.append(fn(*args))
+        return output
 
     async def process_api(
         self,
@@ -1536,6 +1545,7 @@ Received outputs:
         event_id: str | None = None,
         event_data: EventData | None = None,
         in_event_listener: bool = True,
+        explicit_call: bool = False,
     ) -> dict[str, Any]:
         """
         Processes API calls from the frontend. First preprocesses the data,
@@ -1548,6 +1558,8 @@ Received outputs:
             iterators: the in-progress iterators for each generator function (key is function index)
             event_id: id of event that triggered this API call
             event_data: data associated with the event trigger itself
+            in_event_listener: whether this API call is being made in response to an event listener
+            explicit_call: whether this call is being made directly by calling the Blocks function, instead of through an event listener or API route
         Returns: None
         """
         block_fn = self.fns[fn_index]
@@ -1575,6 +1587,7 @@ Received outputs:
                 inputs,
                 fn_index,
                 state,
+                explicit_call,
                 limiter=self.limiter,
             )
             result = await self.call_function(
@@ -1603,7 +1616,12 @@ Received outputs:
                 inputs = []
             else:
                 inputs = await anyio.to_thread.run_sync(
-                    self.preprocess_data, fn_index, inputs, state, limiter=self.limiter
+                    self.preprocess_data,
+                    fn_index,
+                    inputs,
+                    state,
+                    explicit_call,
+                    limiter=self.limiter,
                 )
             was_generating = old_iterator is not None
             result = await self.call_function(

programming_language: Python
filename: processing_utils.py
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,7 @@ from PIL import Image, ImageOps, PngImagePlugin
 
 from gradio import wasm_utils
 from gradio.data_classes import FileData, GradioModel, GradioRootModel
-from gradio.utils import abspath
+from gradio.utils import abspath, get_upload_folder, is_in_or_equal
 
 with warnings.catch_warnings():
     warnings.simplefilter("ignore")  # Ignore pydub warning if ffmpeg is not installed
@@ -241,6 +241,7 @@ def move_files_to_cache(
     block: Component,
     postprocess: bool = False,
     add_urls=False,
+    check_in_upload_folder=False,
 ) -> dict:
     """Move any files in `data` to cache and (optionally), adds URL prefixes (/file=...) needed to access the cached file.
     Also handles the case where the file is on an external Gradio app (/proxy=...).
@@ -252,6 +253,8 @@ def move_files_to_cache(
         block: The component whose data is being processed
         postprocess: Whether its running from postprocessing
         root_url: The root URL of the local server, if applicable
+        add_urls: Whether to add URLs to the payload
+        check_in_upload_folder: If True, instead of moving the file to cache, checks if the file is in already in cache (exception if not).
     """
 
     def _move_to_cache(d: dict):
@@ -264,6 +267,14 @@ def move_files_to_cache(
             payload.path = payload.url
         elif not block.proxy_url:
             # If the file is on a remote server, do not move it to cache.
+            if check_in_upload_folder and not client_utils.is_http_url_like(
+                payload.path
+            ):
+                path = os.path.abspath(payload.path)
+                if not is_in_or_equal(path, get_upload_folder()):
+                    raise ValueError(
+                        f"File {path} is not in the upload folder and cannot be accessed."
+                    )
             temp_file_path = block.move_resource_to_block_cache(payload.path)
             if temp_file_path is None:
                 raise ValueError("Did not determine a file path for the resource.")

programming_language: Python
filename: routes.py
change_type: ModificationType.MODIFY
diff: @@ -17,7 +17,6 @@ import mimetypes
 import os
 import posixpath
 import secrets
-import tempfile
 import threading
 import time
 import traceback
@@ -67,9 +66,7 @@ from gradio.route_utils import (  # noqa: F401
     move_uploaded_files_to_cache,
 )
 from gradio.state_holder import StateHolder
-from gradio.utils import (
-    get_package_version,
-)
+from gradio.utils import get_package_version, get_upload_folder
 
 if TYPE_CHECKING:
     from gradio.blocks import Block
@@ -136,9 +133,7 @@ class App(FastAPI):
         self.cookie_id = secrets.token_urlsafe(32)
         self.queue_token = secrets.token_urlsafe(32)
         self.startup_events_triggered = False
-        self.uploaded_file_dir = os.environ.get("GRADIO_TEMP_DIR") or str(
-            (Path(tempfile.gettempdir()) / "gradio").resolve()
-        )
+        self.uploaded_file_dir = get_upload_folder()
         self.change_event: None | threading.Event = None
         self._asyncio_tasks: list[asyncio.Task] = []
         # Allow user to manually set `docs_url` and `redoc_url`

programming_language: Python
filename: utils.py
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ import json.decoder
 import os
 import pkgutil
 import re
+import tempfile
 import threading
 import time
 import traceback
@@ -1082,3 +1083,9 @@ def diff(old, new):
         return edits
 
     return compare_objects(old, new)
+
+
+def get_upload_folder() -> str:
+    return os.environ.get("GRADIO_TEMP_DIR") or str(
+        (Path(tempfile.gettempdir()) / "gradio").resolve()
+    )

programming_language: Python