CVE-2024-4941 (Python, published 2024-06-06T18:15Z)
Description: value: A local file inclusion vulnerability exists in the JSON component of gradio-app/gradio version 4.25. The vulnerability arises from improper input validation in the `postprocess()` function within `gradio/components/json_component.py`, where a user-controlled string is parsed as JSON. If the parsed JSON object contains a `path` key, the specified file is moved to a temporary directory, making it possible to retrieve it later via the `/file=..` endpoint. This issue is due to the `processing_utils.move_files_to_cache()` function traversing any object passed to it, looking for a dictionary with a `path` key, and then copying the specified file to a temporary directory. The vulnerability can be exploited by an attacker to read files on the remote system, posing a significant security risk.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/gradio-app/gradio
Files changed:
.changeset/gentle-socks-pay.md
gradio/components/base.py
gradio/components/json_component.py
gradio/data_classes.py
gradio/helpers.py
gradio/processing_utils.py
test/components/test_json.py
test/test_helpers.py
test/test_processing_utils.py
Commit message: Ensure JSON component outputs handled properly in postprocess (#8292)

* Add code

* Json postprocess

* add changeset

* add changeset

* Fix json tests

* fix flag

* Address comments

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>

Vulnerable code:
from gradio.data_classes import GradioDataModel
    def postprocess(self, value: dict | list | str | None) -> dict | list | None:
            return orjson.loads(value)
            return orjson.loads(
                orjson.dumps(
                    value,
                    option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATETIME,
                    default=str,
    from pydantic import BaseModel, RootModel, ValidationError
from gradio.data_classes import FileData, GradioModel, GradioRootModel

        )["data"][0] == {
        assert postprocessed_value == expected
        assert json.loads(json.dumps(postprocessed_value)) == expected
from gradio import utils
from gradio import processing_utils, utils

Fixed code:
---
"gradio": patch
---

fix:Ensure JSON component outputs handled properly in postprocess
from gradio.data_classes import GradioDataModel, JsonData
        if isinstance(payload, JsonData):
            payload = payload.model_dump()
from gradio.data_classes import JsonData
    def postprocess(self, value: dict | list | str | None) -> JsonData | None:
            return JsonData(orjson.loads(value))
            return JsonData(
                orjson.loads(
                    orjson.dumps(
                        value,
                        option=orjson.OPT_SERIALIZE_NUMPY
                        | orjson.OPT_PASSTHROUGH_DATETIME,
                        default=str,
                    )
    from pydantic import BaseModel, JsonValue, RootModel, ValidationError
    JsonValue = Any

class JsonData(RootModel):
    """JSON data returned from a component that should not be modified further."""

    root: JsonValue


                processing_utils.check_all_files_in_cache(event_data._data)
from gradio.data_classes import FileData, GradioModel, GradioRootModel, JsonData
from gradio.exceptions import Error
def check_all_files_in_cache(data: JsonData):
    def _in_cache(d: dict):
        if (
            (path := d.get("path", ""))
            and not client_utils.is_http_url_like(path)
            and not is_in_or_equal(path, get_upload_folder())
        ):
            raise Error(
                f"File {path} is not in the cache folder and cannot be accessed."
            )

    client_utils.traverse(data, _in_cache, client_utils.is_file_obj)


        )["data"][0].model_dump() == {
        if postprocessed_value is None:
            assert value is None
        else:
            assert postprocessed_value.model_dump() == expected
            assert json.loads(json.dumps(postprocessed_value.model_dump())) == expected
from gradio import helpers, utils


def test_check_event_data_in_cache():
    def get_select_index(evt: gr.SelectData):
        return evt.index

    with pytest.raises(gr.Error):
        helpers.special_args(
            get_select_index,
            inputs=[],
            event_data=helpers.EventData(
                None,
                {
                    "index": {"path": "foo", "meta": {"_type": "gradio.FileData"}},
                    "value": "whatever",
                },
            ),
        )
from gradio import components, data_classes, processing_utils, utils


@pytest.mark.asyncio
async def test_json_data_not_moved_to_cache():
    data = data_classes.JsonData(
        root={
            "file": {
                "path": "path",
                "url": "/file=path",
                "meta": {"_type": "gradio.FileData"},
            }
        }
    )
    assert (
        processing_utils.move_files_to_cache(data, components.Number(), False) == data
    )
    assert processing_utils.move_files_to_cache(data, components.Number(), True) == data
    assert (
        await processing_utils.async_move_files_to_cache(
            data, components.Number(), False
        )
        == data
    )
    assert (
        await processing_utils.async_move_files_to_cache(
            data, components.Number(), True
        )
        == data
    )