CVE-2024-2206 (Other, published 2024-03-27T01:15Z)
Description: value: An SSRF vulnerability exists in the gradio-app/gradio due to insufficient validation of user-supplied URLs in the `/proxy` route. Attackers can exploit this vulnerability by manipulating the `self.replica_urls` set through the `X-Direct-Url` header in requests to the `/` and `/config` routes, allowing the addition of arbitrary URLs for proxying. This flaw enables unauthorized proxying of requests and potential access to internal endpoints within the Hugging Face space. The issue arises from the application's inadequate checking of safe URLs in the `build_proxy_request` function.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Repository: https://github.com/gradio-app/gradio
Files changed:
.changeset/strong-chefs-study.md
client/js/src/client.ts
client/js/src/index.ts
client/js/src/upload.ts
client/python/gradio_client/utils.py
gradio/blocks.py
gradio/components/base.py
gradio/components/login_button.py
gradio/data_classes.py
gradio/processing_utils.py
gradio/route_utils.py
gradio/routes.py
guides/05_custom-components/05_frontend.md
guides/05_custom-components/07_pdf-component-example.md
js/annotatedimage/Index.svelte
js/app/src/Index.svelte
js/audio/Index.svelte
js/chatbot/Chatbot.test.ts
js/chatbot/Index.svelte
js/file/Index.svelte
js/gallery/Gallery.test.ts
js/gallery/Index.svelte
js/gallery/shared/Gallery.svelte
js/image/Index.svelte
js/image/shared/ImagePreview.svelte
js/image/shared/ImageUploader.svelte
js/imageeditor/Index.svelte
js/imageeditor/shared/InteractiveImageEditor.svelte
js/imageeditor/shared/tools/Sources.svelte
js/model3D/Index.svelte
js/simpleimage/Index.svelte
js/simpleimage/shared/ImageUploader.svelte
js/video/Index.svelte
js/video/Video.test.ts
test/test_components.py
Commit message: [WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component (#7183)

* processing

* add changeset

* changes

* add changeset

* add changeset

* changes

* changes

* clean

* changes

* add changeset

* add changeset

* root url

* refactor

* testing

* testing

* log

* logs

* fix

* format

* add changeset

* remove

* add root

* format

* apply to everything

* annoying fix

* fixes

* lint

* fixes

* fixes

* fixes

* fix tests

* fix js tests

* format

* fix python tests

* clean guides

* add changeset

* add changeset

* simplify

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: pngwn <hello@pngwn.io>

Vulnerable code:
import { FileData, normalise_file } from "./upload";
			normalise_files?: boolean;
			normalise_files?: boolean;
		} = { normalise_files: true }
			const { status_callback, hf_token, normalise_files } = options;
			const transform_files = normalise_files ?? true;
									const data = transform_files
										? transform_output(
												output.data,
												api_info,
												config.root,
												config.root_url
										  )
										: output.data;
										data: transform_files
											? transform_output(
													data.data,
													api_info,
													config.root,
													config.root_url
											  )
											: data.data,
										data: transform_files
											? transform_output(
													data.data,
													api_info,
													config.root,
													config.root_url
											  )
											: data.data,
													data: transform_files
														? transform_output(
																data.data,
																api_info,
																config.root,
																config.root_url
														  )
														: data.data,
function transform_output(
	data: any[],
	api_info: any,
	root_url: string,
	remote_url?: string
): unknown[] {
	return data.map((d, i) => {
		if (api_info?.returns?.[i]?.component === "File") {
			return normalise_file(d, root_url, remote_url);
		} else if (api_info?.returns?.[i]?.component === "Gallery") {
			return d.map((img) => {
				return Array.isArray(img)
					? [normalise_file(img[0], root_url, remote_url), img[1]]
					: [normalise_file(img, root_url, remote_url), null];
			});
		} else if (typeof d === "object" && d.path) {
			return normalise_file(d, root_url, remote_url);
		}
		return d;
	});
}

	normalise_file,
export function normalise_file(
	file: FileData | null,
	server_url: string,
	proxy_url: string | null
): FileData | null;

export function normalise_file(
	file: FileData[] | null,
	server_url: string,
	proxy_url: string | null
): FileData[] | null;

export function normalise_file(
	file: FileData[] | FileData | null,
	server_url: string, // root: string,
	proxy_url: string | null // root_url: string | null
): FileData[] | FileData | null;

export function normalise_file(
	file: FileData[] | FileData | null,
	server_url: string, // root: string,
	proxy_url: string | null // root_url: string | null
): FileData[] | FileData | null {
	if (file == null) {
		return null;
	}

	if (Array.isArray(file)) {
		const normalized_file: (FileData | null)[] = [];

		for (const x of file) {
			if (x == null) {
				normalized_file.push(null);
			} else {
				normalized_file.push(normalise_file(x, server_url, proxy_url));
			}
		}

		return normalized_file as FileData[];
	}

	if (file.is_stream) {
		if (proxy_url == null) {
			return new FileData({
				...file,
				url: server_url + "/stream/" + file.path
			});
		}
		return new FileData({
			...file,
			url: "/proxy=" + proxy_url + "stream/" + file.path
		});
	}

	return new FileData({
		...file,
		url: get_fetchable_url_or_file(file.path, server_url, proxy_url)
	});
}

							const file = new FileData({ ...file_data[i], path: f });

							return normalise_file(file, root, null);
FILE_DATA = "Dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None)"
                        inputs[i], block
        self.value = move_files_to_cache(initial_value, self, postprocess=True)  # type: ignore
            return LoginButton(value=self.value, interactive=True)
def move_files_to_cache(data: Any, block: Component, postprocess: bool = False):
    """Move files to cache and replace the file path with the cache path.
    Runs after .postprocess(), after .process_example(), and before .preprocess().
        block: The component
import shutil

def move_uploaded_files_to_cache(files: list[str], destinations: list[str]) -> None:
    for file, dest in zip(files, destinations):
        shutil.move(file, dest)


            root_path = (
                request.scope.get("root_path")
                or request.headers.get("X-Direct-Url")
                or ""
            )
                config["root"] = route_utils.strip_url(root_path)
            root_path = (
                request.scope.get("root_path")
                or request.headers.get("X-Direct-Url")
                or ""
            )
            config["root"] = route_utils.strip_url(root_path)
The `@gradio/client` npm package provides the `upload`, `prepare_files`, and `normalise_file` utility functions to help you do this.
The `normalise_file` function will generate the correct URL for your component to fetch the file from and set it to the `data` property of the `FileData.`


Tip: Be sure you call `normalise_file` whenever your files are updated!


    import { upload, prepare_files, normalise_file, type FileData } from "@gradio/client";
    $: value: normalise_file(uploaded_files, root)

	import { type FileData, normalise_file, Upload, ModifyUpload } from "@gradio/upload";
    import { normalise_file } from "@gradio/client";
    // Compute the url to fetch the file from the backend
    // whenever a new value is passed in.
    $: _value = normalise_file(value, root, proxy_url);

	import { type FileData, normalise_file } from "@gradio/client";
	export let root: string;
	export let proxy_url: string;
				image: normalise_file(value.image, root, proxy_url) as FileData,
					image: normalise_file(ann.image, root, proxy_url) as FileData,
			status_callback: handle_status,
			normalise_files: false
							status_callback: handle_status,
							normalise_files: false
	import { normalise_file } from "@gradio/client";
	export let proxy_url: null | string;
	let old_value: null | FileData | string = null;
	let _value: null | FileData;
	$: _value = normalise_file(value, root, proxy_url);
			value={_value}
			value={_value}
	import { normalise_file, type FileData } from "@gradio/client";
			file: normalise_file(message?.file, root, proxy_url) as FileData,
	import { normalise_file, type FileData } from "@gradio/client";
	export let proxy_url: null | string;
	$: _value = normalise_file(value, root, proxy_url);
	let old_value = _value;
	$: if (JSON.stringify(old_value) !== JSON.stringify(_value)) {
		old_value = _value;
			value={_value}
			value={_value}
						path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					}
						path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					}
						path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					}
	export let proxy_url: null | string;
			{root}
			{proxy_url}
	import { normalise_file, type FileData } from "@gradio/client";
	export let root = "";
	export let proxy_url: null | string = null;
					image: normalise_file(data.image, root, proxy_url) as FileData,
	import { normalise_file } from "@gradio/client";
	$: _value = normalise_file(value, root, proxy_url);
	export let proxy_url: null | string;
	$: url = _value?.url;
	$: url, gradio.dispatch("change");
			value={_value}
		variant={_value === null ? "dashed" : "solid"}
	import { type FileData, normalise_file } from "@gradio/client";
	import { type FileData, normalise_file } from "@gradio/client";
		value = normalise_file(detail, root, null);
	$: value && !value.url && (value = normalise_file(value, root, null));

	import { normalise_file } from "@gradio/client";
	export let proxy_url: string;
			value={normalise_file(value?.composite || null, root, proxy_url)}
			{proxy_url}
	import {
		prepare_files,
		upload,
		normalise_file,
		type FileData
	} from "@gradio/client";
	export let proxy_url: string;
				background_file={normalise_file(
					value?.background || null,
					root,
					proxy_url
				)}
	<Layers
		layer_files={normalise_file(value?.layers || null, root, proxy_url)}
	/>
	import type { FileData, normalise_file } from "@gradio/client";
	import { normalise_file, type FileData } from "@gradio/client";
	export let proxy_url: null | string;
	let _value: null | FileData;
	$: _value = normalise_file(value, root, proxy_url);

				value={_value}
			value={_value}
	import { normalise_file } from "@gradio/client";
	export let root: string;
	export let proxy_url: null | string;

	$: _value = normalise_file(value, root, proxy_url);
	$: url = _value?.url;
	$: url, gradio.dispatch("change");
			value={_value}
		variant={_value === null ? "dashed" : "solid"}
	import { type FileData, normalise_file } from "@gradio/client";
		value = normalise_file(detail, root, null);
	$: value && !value.url && (value = normalise_file(value, root, null));
	import { normalise_file, type FileData } from "@gradio/client";
	export let proxy_url: null | string;
			_video = normalise_file(value.video, root, proxy_url);
			_subtitle = normalise_file(value.subtitles, root, proxy_url);
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
			"https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
			"https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
					path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
				}
				path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
			},
			subtitles: {
				path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"

Fixed code:
---
"@gradio/annotatedimage": minor
"@gradio/app": minor
"@gradio/audio": minor
"@gradio/chatbot": minor
"@gradio/client": minor
"@gradio/file": minor
"@gradio/gallery": minor
"@gradio/image": minor
"@gradio/imageeditor": minor
"@gradio/model3d": minor
"@gradio/simpleimage": minor
"@gradio/video": minor
"gradio": minor
"gradio_client": minor
---

feat:[WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component
import { FileData } from "./upload";
		} = {}
			const { status_callback, hf_token } = options;
									const data = output.data;
										data: data.data,
										data: data.data,
													data: data.data,
							const file = new FileData({
								...file_data[i],
								path: f,
								url: root + "/file=" + f
							});
							return file;
FILE_DATA = "Dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None, is_stream: bool)"
def is_file_obj_with_url(d):
    return (
        isinstance(d, dict) and "path" in d and "url" in d and isinstance(d["url"], str)
    )


                        inputs[i], block, add_urls=True
                    add_urls=True,
        self.value = move_files_to_cache(
            initial_value,
            self,  # type: ignore
            postprocess=True,
            add_urls=True,
        )
            return LoginButton(value=self.value, interactive=True)  # type: ignore
    is_stream: bool = False
def move_files_to_cache(
    data: Any,
    block: Component,
    postprocess: bool = False,
    add_urls=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=...).
    Runs after .postprocess() and before .preprocess().
        block: The component whose data is being processed
        root_url: The root URL of the local server, if applicable

        if add_urls:
            url_prefix = "/stream/" if payload.is_stream else "/file="
            if block.proxy_url:
                url = f"/proxy={block.proxy_url}{url_prefix}{temp_file_path}"
            elif client_utils.is_http_url_like(
                temp_file_path
            ) or temp_file_path.startswith(f"{url_prefix}"):
                url = temp_file_path
            else:
                url = f"{url_prefix}{temp_file_path}"
            payload.url = url

def add_root_url(data, root_url) -> dict:
    def _add_root_url(file_dict: dict):
        if not client_utils.is_http_url_like(file_dict["url"]):
            file_dict["url"] = f'{root_url}{file_dict["url"]}'
        return file_dict

    return client_utils.traverse(data, _add_root_url, client_utils.is_file_obj_with_url)


import shutil


def move_uploaded_files_to_cache(files: list[str], destinations: list[str]) -> None:
    for file, dest in zip(files, destinations):
        shutil.move(file, dest)
from gradio.processing_utils import add_root_url
    move_uploaded_files_to_cache,
            root_path = route_utils.strip_url(str(request.url))
                if "root" not in config:
                    config["root"] = root_path
                    config = add_root_url(config, root_path)
            root_path = route_utils.strip_url(str(request.url))[:-7]
            if "root" not in config:
                config["root"] = route_utils.strip_url(root_path)
                config = add_root_url(config, root_path)
            root_path = app.get_blocks().config.get("root", "")
            output = add_root_url(output, route_utils.strip_url(root_path))
            root_path = app.get_blocks().config.get("root", "")
                            add_root_url(message, route_utils.strip_url(root_path))
The `@gradio/client` npm package provides the `upload` and `prepare_files` utility functions to help you do this.
    import { upload, prepare_files, type FileData } from "@gradio/client";
	import { type FileData, Upload, ModifyUpload } from "@gradio/upload";
	import { type FileData } from "@gradio/client";
				image: value.image as FileData,
					image: ann.image as FileData,
			status_callback: handle_status
							status_callback: handle_status
	let old_value: null | FileData = null;
			{value}
			{value}
					url: "https://gradio-builds.s3.amazonaws.com/demo-files/cheetah1.jpg",
						url: "https://gradio-builds.s3.amazonaws.com/demo-files/video_sample.mp4",
					url: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
					url: "https://gradio-builds.s3.amazonaws.com/demo-files/titanic.csv",
	import type { FileData } from "@gradio/client";
			file: message?.file as FileData,
	import type { FileData } from "@gradio/client";
	let old_value = value;
	$: if (JSON.stringify(old_value) !== JSON.stringify(value)) {
		old_value = value;
			{value}
			{value}
						path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
						url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					},
					caption: null
						path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
						url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					},
					caption: null
						path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
						url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					},
					caption: null
	import { FileData } from "@gradio/client";
					image: data.image as FileData,
	let old_value: null | FileData = null;
	$: {
		if (JSON.stringify(value) !== JSON.stringify(old_value)) {
			old_value = value;
			gradio.dispatch("change");
		}
	}
			{value}
		variant={value === null ? "dashed" : "solid"}
	import { type FileData } from "@gradio/client";
	import type { FileData } from "@gradio/client";
		value = detail;
			value={value?.composite || null}
	import { prepare_files, upload, type FileData } from "@gradio/client";
				background_file={value?.background || null}
	<Layers layer_files={value?.layers || null} />
	import type { FileData } from "@gradio/client";
	import type { FileData } from "@gradio/client";
				{value}
			{value}
	export let root: string;
	$: value, gradio.dispatch("change");
			{value}
		variant={value === null ? "dashed" : "solid"}
	import type { FileData } from "@gradio/client";
		value = detail;
	import type { FileData } from "@gradio/client";
			_video = value.video;
			_subtitle = value.subtitles;
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
			"https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
			"https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
					path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
					url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
				},
				subtitles: null
				path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
				url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
                "is_stream": False,
                "is_stream": False,
                "is_stream": False,
                    "is_stream": False,
                    "is_stream": False,
                    "is_stream": False,
                    "is_stream": False,
                    "is_stream": False,