diff --git a/.changeset/four-swans-throw.md b/.changeset/four-swans-throw.md new file mode 100644 index 0000000000000..d31cb76317992 --- /dev/null +++ b/.changeset/four-swans-throw.md @@ -0,0 +1,5 @@ +--- +"gradio": minor +--- + +feat:Enforce `meta` key present during preprocess in FileData payloads diff --git a/gradio/blocks.py b/gradio/blocks.py index e464037eb036e..9b36eb4e39076 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -18,12 +18,7 @@ from collections.abc import AsyncIterator, Callable, Coroutine, Sequence, Set from pathlib import Path from types import ModuleType -from typing import ( - TYPE_CHECKING, - Any, - Literal, - cast, -) +from typing import TYPE_CHECKING, Any, Literal, Union, cast from urllib.parse import urlparse, urlunparse import anyio @@ -1702,10 +1697,12 @@ async def preprocess_data( 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 - inputs_cached = block.data_model(**inputs_cached) # type: ignore - elif issubclass(block.data_model, GradioRootModel): # type: ignore - inputs_cached = block.data_model(root=inputs_cached) # type: ignore + data_model = cast( + Union[GradioModel, GradioRootModel], block.data_model + ) + inputs_cached = data_model.model_validate( + inputs_cached, context={"validate_meta": True} + ) processed_input.append(block.preprocess(inputs_cached)) else: processed_input = inputs diff --git a/gradio/data_classes.py b/gradio/data_classes.py index 268509e857a66..15c3f3b264753 100644 --- a/gradio/data_classes.py +++ b/gradio/data_classes.py @@ -21,13 +21,15 @@ from fastapi import Request from gradio_client.documentation import document -from gradio_client.utils import traverse +from gradio_client.utils import is_file_obj_with_meta, traverse from pydantic import ( BaseModel, GetCoreSchemaHandler, GetJsonSchemaHandler, RootModel, ValidationError, + ValidationInfo, + model_validator, ) from pydantic.json_schema import JsonSchemaValue from pydantic_core import core_schema @@ -227,6 +229,19 @@ class FileData(GradioModel): is_stream: bool = False meta: dict = {"_type": "gradio.FileData"} + @model_validator(mode="before") + @classmethod + def validate_model(cls, v, info: ValidationInfo): + if ( + info.context + and info.context.get("validate_meta") + and not is_file_obj_with_meta(v) + ): + raise ValueError( + "The 'meta' field must be explicitly provided in the input data and be equal to {'_type': 'gradio.FileData'}." + ) + return v + @property def is_none(self) -> bool: """ diff --git a/test/test_routes.py b/test/test_routes.py index d085405e193ec..65c5985d76e1f 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -339,6 +339,7 @@ def test_get_file_created_by_app(self, test_client): { "path": file_response.json()[0], "size": os.path.getsize("test/test_files/alphabet.txt"), + "meta": {"_type": "gradio.FileData"}, } ], "fn_index": 0, @@ -1613,3 +1614,30 @@ def victim(url, results): t.join() assert not any(results), "attacker was able to modify a victim's config root url" + + +def test_file_without_meta_key_not_moved(): + demo = gr.Interface( + fn=lambda s: str(s), inputs=gr.File(type="binary"), outputs="textbox" + ) + + app, _, _ = demo.launch(prevent_thread_lock=True) + test_client = TestClient(app) + try: + with test_client: + req = test_client.post( + "gradio_api/run/predict", + json={ + "data": [ + { + "path": "test/test_files/alphabet.txt", + "orig_name": "test.txt", + "size": 4, + "mime_type": "text/plain", + } + ] + }, + ) + assert req.status_code == 500 + finally: + demo.close()