CVE-2024-1561: Path Traversal: '\..\filename' (CWE-29)
value: An issue was discovered in gradio-app/gradio, where the `/component_server` endpoint improperly allows the invocation of any method on a `Component` class with attacker-controlled arguments. Specifically, by exploiting the `move_resource_to_block_cache()` method of the `Block` class, an attacker can copy any file on the filesystem to a temporary directory and subsequently retrieve it. This vulnerability enables unauthorized local file read access, posing a significant risk especially when the application is exposed to the internet via `launch(share=True)`, thereby allowing remote attackers to read files on the host machine. Furthermore, gradio apps hosted on `huggingface.co` are also affected, potentially leading to the exposure of sensitive information such as API keys and credentials stored in environment variables.
CWE description: The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize '\..\filename' (leading backslash dot dot) sequences that can resolve to a location that is outside of that directory.
Published: 2024-04-16T00:15Z
Repository: https://github.com/gradio-app/gradio
Language: Python
Files changed: .changeset/ten-chefs-argue.md, gradio/components/file_explorer.py, gradio/routes.py, test/test_routes.py
Commit date: 2023-12-27 14:16:56 -0800
Commit message: Component Server fix (#6884)

* changes

* add changeset

* Update gradio/routes.py

Co-authored-by: Aarni Koskela <akx@iki.fi>

* changes

* changes

---------

Co-authored-by: Ali Abid <ubuntu@ip-172-31-25-241.us-west-2.compute.internal>
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Aarni Koskela <akx@iki.fi>
Diff: diff --git a/.changeset/ten-chefs-argue.md b/.changeset/ten-chefs-argue.md
new file mode 100644
index 000000000..80f81b862
--- /dev/null
+++ b/.changeset/ten-chefs-argue.md
@@ -0,0 +1,5 @@
+---
+"gradio": minor
+---
+
+feat:Component Server fix
diff --git a/gradio/components/file_explorer.py b/gradio/components/file_explorer.py
index 7ed5909fe..d7b2fac8a 100644
--- a/gradio/components/file_explorer.py
+++ b/gradio/components/file_explorer.py
@@ -137,14 +137,12 @@ class FileExplorer(Component):
             root.append(self._strip_root(file).split(os.path.sep))
 
         return FileExplorerData(root=root)
 
     @server
-    def ls(self, value=None) -> list[dict[str, str]] | None:
+    def ls(self, _=None) -> list[dict[str, str]] | None:
         """
-        Parameters:
-            value: file path as a list of strings for each directory level relative to the root.
         Returns:
             tuple of list of files in directory, then list of folders in directory
         """
 
         def expand_braces(text, seen=None):
diff --git a/gradio/routes.py b/gradio/routes.py
index 293dfec69..423572839 100644
--- a/gradio/routes.py
+++ b/gradio/routes.py
@@ -697,11 +697,16 @@ class App(FastAPI):
             block: Block
             if component_id in state:
                 block = state[component_id]
             else:
                 block = app.get_blocks().blocks[component_id]
-            fn = getattr(block, body.fn_name)
+            fn = getattr(block, body.fn_name, None)
+            if fn is None or not getattr(fn, "_is_server_fn", False):
+                raise HTTPException(
+                    status_code=status.HTTP_404_NOT_FOUND,
+                    detail="Function not found.",
+                )
             return fn(body.data)
 
         @app.get(
             "/queue/status",
             dependencies=[Depends(login_check)],
diff --git a/test/test_routes.py b/test/test_routes.py
index 08bd596d5..82c46d3b4 100644
--- a/test/test_routes.py
+++ b/test/test_routes.py
@@ -829,5 +829,36 @@ class TestShowAPI:
     def test_show_api_true_when_is_wasm_false(self):
         interface = Interface(lambda x: x, "text", "text", examples=[["hannah"]])
         assert (
             interface.show_api is True
         ), "show_api should be True when IS_WASM is False"
+
+
+def test_component_server_endpoints(connect):
+    here = os.path.dirname(os.path.abspath(__file__))
+    with gr.Blocks() as demo:
+        file_explorer = gr.FileExplorer(root=here)
+
+    with closing(demo) as io:
+        app, _, _ = io.launch(prevent_thread_lock=True)
+        client = TestClient(app)
+        success_req = client.post(
+            "/component_server/",
+            json={
+                "session_hash": "123",
+                "component_id": file_explorer._id,
+                "fn_name": "ls",
+                "data": None,
+            },
+        )
+        assert success_req.status_code == 200
+        assert len(success_req.json()) > 0
+        fail_req = client.post(
+            "/component_server/",
+            json={
+                "session_hash": "123",
+                "component_id": file_explorer._id,
+                "fn_name": "preprocess",
+                "data": None,
+            },
+        )
+        assert fail_req.status_code == 404

Vulnerable code:     def ls(self, value=None) -> list[dict[str, str]] | None:
        Parameters:
            value: file path as a list of strings for each directory level relative to the root.
            fn = getattr(block, body.fn_name)
Fixed code: ---
"gradio": minor
---

feat:Component Server fix
    def ls(self, _=None) -> list[dict[str, str]] | None:
            fn = getattr(block, body.fn_name, None)
            if fn is None or not getattr(fn, "_is_server_fn", False):
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail="Function not found.",
                )


def test_component_server_endpoints(connect):
    here = os.path.dirname(os.path.abspath(__file__))
    with gr.Blocks() as demo:
        file_explorer = gr.FileExplorer(root=here)

    with closing(demo) as io:
        app, _, _ = io.launch(prevent_thread_lock=True)
        client = TestClient(app)
        success_req = client.post(
            "/component_server/",
            json={
                "session_hash": "123",
                "component_id": file_explorer._id,
                "fn_name": "ls",
                "data": None,
            },
        )
        assert success_req.status_code == 200
        assert len(success_req.json()) > 0
        fail_req = client.post(
            "/component_server/",
            json={
                "session_hash": "123",
                "component_id": file_explorer._id,
                "fn_name": "preprocess",
                "data": None,
            },
        )
        assert fail_req.status_code == 404