Skip to content

Commit 08b5159

Browse files
freddyaboultonpngwnhannahblairgradio-pr-botaliabd
authored
Some tweaks to is_in_or_equal (#9020)
* Add code * file_explorer test * dont use fastapi * Add code * Update requirements.txt * Ci security tweaks (#9010) * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * Ci security tweaks (#9012) * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * :[ * add cursor styling (#9003) * Add min/max-imize button to gr.Image and gr.Gallery (#8964) * add max/min-imize and zoom in and out to image preview * add full screen icon to gallery * add stories * add changeset * use native full screen api * remove zoom in/out * add changeset * tweaks * remove zoom prop * fix ui test * add annotated image btns * add changeset * format * ruff format * fix test * remove * tweak * fix test * format * amend bool check --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: pngwn <hello@pngwn.io> * Ci security tweaks (#9014) * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * asd * :[ * asd * asd (#9015) * Ci security tweaks take seventy three (#9016) * asd * asd * asd * Ci security tweaks take seventy three (#9017) * asd * asd * asd * asd * Ci security tweaks take seventy three (#9018) * asd * asd * asd * asd * adsa * asd * Ci security tweaks take seventy three (#9019) * asd * asd * asd * asd * adsa * asd * asd * Website fixes for mobile (#8857) * better header for mobile * add changeset * nicer border * style header * better search overlay * responsive changes * more mobile responsiveness * docs and guides mobile responsive * formatting * fix get started button * fix header * test without lite * formatting * secondary menu docs * fix type overflow in paramtable * playground header * playground on mobile * small changes * formatting * formatting --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> * Ci security tweaks take seventy three (#9025) * asd * asd * asd * asd * adsa * asd * asd * asd * Ci security tweaks take seventy three (#9026) * asd * asd * asd * asd * adsa * asd * asd * asd * fix * asd (#9027) * fix (#9028) * Ci statuses (#9030) * fix * asd * Update tests * add changeset * Add code * add changeset * Comments --------- Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: Hannah <hannahblair@users.noreply.github.com> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Ali Abdalla <ali.si3luwa@gmail.com>
1 parent c12f600 commit 08b5159

13 files changed

+246
-74
lines changed

.changeset/ten-lands-change.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gradio": patch
3+
---
4+
5+
feat:Some tweaks to is_in_or_equal

gradio/blocks.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@
5656
get_render_context,
5757
set_render_context,
5858
)
59-
from gradio.data_classes import BlocksConfigDict, FileData, GradioModel, GradioRootModel
59+
from gradio.data_classes import (
60+
BlocksConfigDict,
61+
DeveloperPath,
62+
FileData,
63+
GradioModel,
64+
GradioRootModel,
65+
)
6066
from gradio.events import (
6167
EventData,
6268
EventListener,
@@ -409,7 +415,7 @@ def __init__(
409415
render=render,
410416
)
411417

412-
TEMPLATE_DIR = "./templates/"
418+
TEMPLATE_DIR = DeveloperPath("./templates/")
413419
FRONTEND_DIR = "../../frontend/"
414420

415421
@property

gradio/components/base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from gradio import utils
2020
from gradio.blocks import Block, BlockContext
2121
from gradio.component_meta import ComponentMeta
22-
from gradio.data_classes import BaseModel, GradioDataModel
22+
from gradio.data_classes import BaseModel, DeveloperPath, GradioDataModel
2323
from gradio.events import EventListener
2424
from gradio.layouts import Form
2525
from gradio.processing_utils import move_files_to_cache
@@ -228,7 +228,7 @@ def __init__(
228228

229229
self.component_class_id = self.__class__.get_component_class_id()
230230

231-
TEMPLATE_DIR = "./templates/"
231+
TEMPLATE_DIR = DeveloperPath("./templates/")
232232
FRONTEND_DIR = "../../frontend/"
233233

234234
def get_config(self):

gradio/components/file_explorer.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from gradio_client.documentation import document
1212

1313
from gradio.components.base import Component, server
14-
from gradio.data_classes import GradioRootModel
14+
from gradio.data_classes import DeveloperPath, GradioRootModel, UserProvidedPath
15+
from gradio.utils import safe_join
1516

1617
if TYPE_CHECKING:
1718
from gradio.components import Timer
@@ -85,7 +86,7 @@ def __init__(
8586
)
8687
root_dir = root
8788
self._constructor_args[0]["root_dir"] = root
88-
self.root_dir = os.path.abspath(root_dir)
89+
self.root_dir = DeveloperPath(os.path.abspath(root_dir))
8990
self.glob = glob
9091
self.ignore_glob = ignore_glob
9192
valid_file_count = ["single", "multiple"]
@@ -202,11 +203,8 @@ def ls(self, subdirectory: list | None = None) -> list[dict[str, str]] | None:
202203

203204
return folders + files
204205

205-
def _safe_join(self, folders):
206-
combined_path = os.path.join(self.root_dir, *folders)
207-
absolute_path = os.path.abspath(combined_path)
208-
if os.path.commonprefix([self.root_dir, absolute_path]) != os.path.abspath(
209-
self.root_dir
210-
):
211-
raise ValueError("Attempted to navigate outside of root directory")
212-
return absolute_path
206+
def _safe_join(self, folders: list[str]):
207+
if not folders or len(folders) == 0:
208+
return self.root_dir
209+
combined_path = UserProvidedPath(os.path.join(*folders))
210+
return safe_join(self.root_dir, combined_path)

gradio/data_classes.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@
88
import shutil
99
from abc import ABC, abstractmethod
1010
from enum import Enum, auto
11-
from typing import Any, Iterator, List, Literal, Optional, Tuple, TypedDict, Union
11+
from typing import (
12+
Any,
13+
Iterator,
14+
List,
15+
Literal,
16+
NewType,
17+
Optional,
18+
Tuple,
19+
TypedDict,
20+
Union,
21+
)
1222

1323
from fastapi import Request
1424
from gradio_client.documentation import document
@@ -21,6 +31,9 @@
2131
except ImportError:
2232
JsonValue = Any
2333

34+
DeveloperPath = NewType("DeveloperPath", str)
35+
UserProvidedPath = NewType("UserProvidedPath", str)
36+
2437

2538
class CancelBody(BaseModel):
2639
session_hash: str

gradio/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,7 @@ def __str__(self):
9898

9999
class ComponentDefinitionError(NotImplementedError):
100100
pass
101+
102+
103+
class InvalidPathError(ValueError):
104+
pass

gradio/route_utils.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
from starlette.types import ASGIApp, Message, Receive, Scope, Send
4343

4444
from gradio import processing_utils, utils
45-
from gradio.data_classes import BlocksConfigDict, PredictBody
45+
from gradio.data_classes import (
46+
BlocksConfigDict,
47+
PredictBody,
48+
)
4649
from gradio.exceptions import Error
4750
from gradio.helpers import EventData
4851
from gradio.state_holder import SessionState

gradio/routes.py

+51-38
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import json
1919
import mimetypes
2020
import os
21-
import posixpath
2221
import secrets
2322
import time
2423
import traceback
@@ -35,6 +34,7 @@
3534
Optional,
3635
Type,
3736
Union,
37+
cast,
3838
)
3939

4040
import fastapi
@@ -73,10 +73,13 @@
7373
ComponentServerBlobBody,
7474
ComponentServerJSONBody,
7575
DataWithFiles,
76+
DeveloperPath,
7677
PredictBody,
7778
ResetBody,
7879
SimplePredictBody,
80+
UserProvidedPath,
7981
)
82+
from gradio.exceptions import InvalidPathError
8083
from gradio.oauth import attach_oauth
8184
from gradio.route_utils import ( # noqa: F401
8285
CustomCORSMiddleware,
@@ -109,9 +112,18 @@
109112

110113
mimetypes.init()
111114

112-
STATIC_TEMPLATE_LIB = files("gradio").joinpath("templates").as_posix() # type: ignore
113-
STATIC_PATH_LIB = files("gradio").joinpath("templates", "frontend", "static").as_posix() # type: ignore
114-
BUILD_PATH_LIB = files("gradio").joinpath("templates", "frontend", "assets").as_posix() # type: ignore
115+
STATIC_TEMPLATE_LIB = cast(
116+
DeveloperPath,
117+
files("gradio").joinpath("templates").as_posix(), # type: ignore
118+
)
119+
STATIC_PATH_LIB = cast(
120+
DeveloperPath,
121+
files("gradio").joinpath("templates", "frontend", "static").as_posix(), # type: ignore
122+
)
123+
BUILD_PATH_LIB = cast(
124+
DeveloperPath,
125+
files("gradio").joinpath("templates", "frontend", "assets").as_posix(), # type: ignore
126+
)
115127
VERSION = get_package_version()
116128

117129

@@ -446,7 +458,7 @@ def get_config(request: fastapi.Request):
446458

447459
@app.get("/static/{path:path}")
448460
def static_resource(path: str):
449-
static_file = safe_join(STATIC_PATH_LIB, path)
461+
static_file = routes_safe_join(STATIC_PATH_LIB, UserProvidedPath(path))
450462
return FileResponse(static_file)
451463

452464
@app.get("/custom_component/{id}/{type}/{file_name}")
@@ -458,7 +470,6 @@ def custom_component_path(
458470
location = next(
459471
(item for item in components if item["component_class_id"] == id), None
460472
)
461-
462473
if location is None:
463474
raise HTTPException(status_code=404, detail="Component not found.")
464475

@@ -470,9 +481,14 @@ def custom_component_path(
470481
if module_path is None or component_instance is None:
471482
raise HTTPException(status_code=404, detail="Component not found.")
472483

473-
path = safe_join(
474-
str(Path(module_path).parent),
475-
f"{component_instance.__class__.TEMPLATE_DIR}/{type}/{file_name}",
484+
requested_path = utils.safe_join(
485+
component_instance.__class__.TEMPLATE_DIR,
486+
UserProvidedPath(f"{type}/{file_name}"),
487+
)
488+
489+
path = routes_safe_join(
490+
DeveloperPath(str(Path(module_path).parent)),
491+
UserProvidedPath(requested_path),
476492
)
477493

478494
key = f"{id}-{type}-{file_name}"
@@ -494,7 +510,7 @@ def custom_component_path(
494510

495511
@app.get("/assets/{path:path}")
496512
def build_resource(path: str):
497-
build_file = safe_join(BUILD_PATH_LIB, path)
513+
build_file = routes_safe_join(BUILD_PATH_LIB, UserProvidedPath(path))
498514
return FileResponse(build_file)
499515

500516
@app.get("/favicon.ico")
@@ -543,7 +559,7 @@ async def file(path_or_url: str, request: fastapi.Request):
543559

544560
is_dir = abs_path.is_dir()
545561

546-
if in_blocklist or is_dir:
562+
if is_dir or in_blocklist:
547563
raise HTTPException(403, f"File not allowed: {path_or_url}.")
548564

549565
created_by_app = False
@@ -1142,7 +1158,14 @@ async def upload_file(
11421158
name = f"tmp{secrets.token_hex(5)}"
11431159
directory = Path(app.uploaded_file_dir) / temp_file.sha.hexdigest()
11441160
directory.mkdir(exist_ok=True, parents=True)
1145-
dest = (directory / name).resolve()
1161+
try:
1162+
dest = utils.safe_join(
1163+
DeveloperPath(str(directory)), UserProvidedPath(name)
1164+
)
1165+
except InvalidPathError as err:
1166+
raise HTTPException(
1167+
status_code=400, detail=f"Invalid file name: {name}"
1168+
) from err
11461169
temp_file.file.close()
11471170
# we need to move the temp file to the cache directory
11481171
# but that's possibly blocking and we're in an async function
@@ -1153,9 +1176,9 @@ async def upload_file(
11531176
os.rename(temp_file.file.name, dest)
11541177
except OSError:
11551178
files_to_copy.append(temp_file.file.name)
1156-
locations.append(str(dest))
1179+
locations.append(dest)
11571180
output_files.append(dest)
1158-
blocks.upload_file_set.add(str(dest))
1181+
blocks.upload_file_set.add(dest)
11591182
if files_to_copy:
11601183
bg_tasks.add_task(
11611184
move_uploaded_files_to_cache, files_to_copy, locations
@@ -1218,32 +1241,22 @@ async def analytics_dashboard(key: str):
12181241
########
12191242

12201243

1221-
def safe_join(directory: str, path: str) -> str:
1222-
"""Safely path to a base directory to avoid escaping the base directory.
1223-
Borrowed from: werkzeug.security.safe_join"""
1224-
_os_alt_seps: List[str] = [
1225-
sep for sep in [os.path.sep, os.path.altsep] if sep is not None and sep != "/"
1226-
]
1227-
1244+
def routes_safe_join(directory: DeveloperPath, path: UserProvidedPath) -> str:
1245+
"""Safely join the user path to the directory while performing some additional http-related checks,
1246+
e.g. ensuring that the full path exists on the local file system and is not a directory"""
12281247
if path == "":
1229-
raise HTTPException(400)
1248+
raise fastapi.HTTPException(400)
12301249
if route_utils.starts_with_protocol(path):
1231-
raise HTTPException(403)
1232-
filename = posixpath.normpath(path)
1233-
fullpath = os.path.join(directory, filename)
1234-
if (
1235-
any(sep in filename for sep in _os_alt_seps)
1236-
or os.path.isabs(filename)
1237-
or filename == ".."
1238-
or filename.startswith("../")
1239-
or os.path.isdir(fullpath)
1240-
):
1241-
raise HTTPException(403)
1242-
1243-
if not os.path.exists(fullpath):
1244-
raise HTTPException(404, "File not found")
1245-
1246-
return fullpath
1250+
raise fastapi.HTTPException(403)
1251+
try:
1252+
fullpath = Path(utils.safe_join(directory, path))
1253+
except InvalidPathError as e:
1254+
raise fastapi.HTTPException(403) from e
1255+
if fullpath.is_dir():
1256+
raise fastapi.HTTPException(403)
1257+
if not fullpath.exists():
1258+
raise fastapi.HTTPException(404)
1259+
return str(fullpath)
12471260

12481261

12491262
def get_types(cls_set: List[Type]):

0 commit comments

Comments
 (0)