Skip to content

Commit 84d08cf

Browse files
authored
Merge commit from fork
Signed-off-by: Frost Ming <me@frostming.com>
1 parent ee0dd74 commit 84d08cf

File tree

10 files changed

+66
-33
lines changed

10 files changed

+66
-33
lines changed

src/_bentoml_sdk/images.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from bentoml._internal.container import split_envs_by_stage
2121
from bentoml._internal.container.frontend.dockerfile import CONTAINER_METADATA
2222
from bentoml._internal.container.frontend.dockerfile import CONTAINER_SUPPORTED_DISTROS
23+
from bentoml._internal.utils.filesystem import chdir
24+
from bentoml._internal.utils.filesystem import resolve_user_filepath
2325
from bentoml.exceptions import BentoMLConfigException
2426
from bentoml.exceptions import BentoMLException
2527

@@ -97,6 +99,7 @@ def requirements_file(self, file_path: str) -> t.Self:
9799
"""
98100
if self.post_commands:
99101
raise BentoMLConfigException("Can't separate adding python requirements")
102+
file_path = resolve_user_filepath(file_path, None)
100103
self.python_requirements += Path(file_path).read_text().rstrip("\n") + "\n"
101104
self._after_pip_install = True
102105
return self
@@ -112,6 +115,7 @@ def pyproject_toml(self, file_path: str = "pyproject.toml") -> t.Self:
112115
"""
113116
if self.post_commands:
114117
raise BentoMLConfigException("Can't separate adding python requirements")
118+
file_path = resolve_user_filepath(file_path, None)
115119
with Path(file_path).open("rb") as f:
116120
pyproject_toml = tomllib.load(f)
117121
dependencies = pyproject_toml.get("project", {}).get("dependencies", {})
@@ -164,7 +168,7 @@ def run_script(self, script: str) -> t.Self:
164168
image = Image("debian:latest").run_script("script.sh")
165169
"""
166170
commands = self.post_commands if self._after_pip_install else self.commands
167-
script = Path(script).resolve().as_posix()
171+
script = resolve_user_filepath(script, None)
168172
# Files under /env/docker will be copied into the env image layer
169173
target_script = (
170174
f"./env/docker/script__{hashlib.md5(script.encode()).hexdigest()}"
@@ -327,8 +331,6 @@ def _freeze_python_requirements(
327331
def populate_image_from_build_config(
328332
image: Image | None, build_config: BentoBuildConfig, build_ctx: str
329333
) -> Image | None:
330-
from bentoml._internal.utils.filesystem import resolve_user_filepath
331-
332334
fallback_message = "fallback to bento v1" if image is None else "it will be ignored"
333335
if not build_config.conda.is_empty():
334336
logger.warning(
@@ -392,12 +394,12 @@ def populate_image_from_build_config(
392394
image.python_packages(
393395
*(f"--find-links {link}" for link in python_options.find_links)
394396
)
395-
if python_options.requirements_txt:
396-
image.requirements_file(
397-
resolve_user_filepath(python_options.requirements_txt, build_ctx)
398-
)
399-
elif python_options.packages:
400-
image.python_packages(*python_options.packages)
401-
if docker_options.setup_script:
402-
image.run_script(docker_options.setup_script)
397+
398+
with chdir(build_ctx):
399+
if python_options.requirements_txt:
400+
image.requirements_file(python_options.requirements_txt)
401+
elif python_options.packages:
402+
image.python_packages(*python_options.packages)
403+
if docker_options.setup_script:
404+
image.run_script(docker_options.setup_script)
403405
return image

src/bentoml/_internal/bento/bento.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from ..types import PathType
3636
from ..utils import normalize_labels_value
3737
from ..utils.cattr import bentoml_cattr
38+
from ..utils.filesystem import resolve_user_filepath
3839
from ..utils.filesystem import safe_remove_dir
3940
from .build_config import BentoBuildConfig
4041
from .build_config import BentoEnvSchema
@@ -349,9 +350,13 @@ def append_model(model: BentoModelInfo) -> None:
349350
build_config.description is not None
350351
and build_config.description.startswith("file:")
351352
):
352-
file_name = build_config.description[5:].strip()
353-
if not ctx_path.joinpath(file_name).exists():
354-
raise InvalidArgument(f"File {file_name} does not exist.")
353+
try:
354+
file_name = resolve_user_filepath(
355+
build_config.description[5:].strip(), str(ctx_path)
356+
)
357+
except (FileNotFoundError, ValueError) as e:
358+
raise InvalidArgument(str(e)) from None
359+
355360
shutil.copy(ctx_path.joinpath(file_name), bento_readme)
356361
elif (
357362
build_config.description is None

src/bentoml/_internal/cloud/deployment.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ def verify(
9595
if self.config_dict:
9696
self.cfg_dict = self.config_dict
9797
elif isinstance(self.config_file, str):
98-
real_path = resolve_user_filepath(self.config_file, self.path_context)
98+
real_path = resolve_user_filepath(
99+
self.config_file, self.path_context, secure=False
100+
)
99101
try:
100102
with open(real_path, "r") as file:
101103
self.cfg_dict = yaml.safe_load(file)
@@ -313,7 +315,7 @@ def get_args_from_config(
313315
cluster = config_dict["cluster"]
314316

315317
if isinstance(config_file, str):
316-
real_path = resolve_user_filepath(config_file, path_context)
318+
real_path = resolve_user_filepath(config_file, path_context, secure=False)
317319
try:
318320
with open(real_path, "r") as file:
319321
file_dict = yaml.safe_load(file)

src/bentoml/_internal/container/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import attr
1515

1616
from ...exceptions import BentoMLException
17-
from ..utils.filesystem import resolve_user_filepath
1817

1918
if TYPE_CHECKING:
2019
from typing_extensions import Self
@@ -56,7 +55,7 @@ def _(self, args: ArgType, opt: str = ""):
5655
def _(self, args: PathType, opt: str = ""):
5756
if args is not None:
5857
if os.path.exists(str(args)):
59-
args = resolve_user_filepath(str(args), ctx=None)
58+
args = os.path.abspath(str(args))
6059
self.extend((f"--{opt}", str(args)))
6160

6261
@construct_args.register(type(None))

src/bentoml/_internal/io_descriptors/file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ async def predict(input: t.IO[t.Any]) -> t.IO[t.Any]:
162162
if isinstance(sample, t.IO):
163163
sample = FileLike[bytes](sample, "<sample>")
164164
elif isinstance(sample, (str, os.PathLike)):
165-
p = resolve_user_filepath(sample, ctx=None)
165+
p = resolve_user_filepath(sample, ctx=None, secure=False)
166166
mime = mimetypes.guess_type(p)[0]
167167
self._mime_type = mime
168168
with open(p, "rb") as f:

src/bentoml/_internal/io_descriptors/image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ async def predict(input: t.IO[t.Any]) -> t.IO[t.Any]:
250250
if LazyType["ext.NpNDArray"]("numpy.ndarray").isinstance(sample):
251251
sample = PIL.Image.fromarray(sample)
252252
elif isinstance(sample, str):
253-
p = resolve_user_filepath(sample, ctx=None)
253+
p = resolve_user_filepath(sample, ctx=None, secure=False)
254254
try:
255255
with open(p, "rb") as f:
256256
sample = PIL.Image.open(f)

src/bentoml/_internal/utils/filesystem.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,26 +111,51 @@ def validate_or_create_dir(*path: PathType) -> None:
111111
path_obj.mkdir(parents=True, exist_ok=True)
112112

113113

114-
def resolve_user_filepath(filepath: str, ctx: t.Optional[str]) -> str:
114+
def resolve_user_filepath(
115+
filepath: str, ctx: t.Optional[str], secure: bool = True
116+
) -> str:
115117
"""Resolve the abspath of a filepath provided by user. User provided file path can:
116118
* be a relative path base on ctx dir
117119
* contain leading "~" for HOME directory
118120
* contain environment variables such as "$HOME/workspace"
119121
"""
120122
# Return if filepath exist after expanduser
121123

122-
_path = os.path.expanduser(os.path.expandvars(filepath))
124+
_path = Path(os.path.expanduser(os.path.expandvars(filepath)))
123125

124126
# Try finding file in ctx if provided
125-
if not os.path.isabs(_path) and ctx:
126-
_path = os.path.expanduser(os.path.join(ctx, filepath))
127-
128-
if os.path.exists(_path):
129-
return os.path.realpath(_path)
130-
131-
raise FileNotFoundError(f"file {filepath} not found")
127+
if not _path.is_absolute():
128+
ctx = os.path.expanduser(ctx) if ctx else os.getcwd()
129+
_path = Path(ctx).joinpath(_path)
130+
elif secure:
131+
raise ValueError(f"Absolute path {filepath} is not allowed")
132+
_path = _path.resolve()
133+
if not _path.exists():
134+
raise FileNotFoundError(f"file {filepath} not found")
135+
if secure:
136+
cwd = Path().resolve()
137+
if not _path.is_relative_to(cwd):
138+
raise ValueError(
139+
f"Accessing file outside of current working directory is not allowed: {_path}"
140+
)
141+
if any(part.startswith(".") for part in _path.parts):
142+
raise ValueError(f"Accessing hidden files is not allowed: {_path}")
143+
if any(_path.is_relative_to(item) for item in ("/etc", "/proc")):
144+
raise ValueError(f"Accessing system files is not allowed: {_path}")
145+
return str(_path)
132146

133147

134148
def safe_remove_dir(path: PathType) -> None:
135149
with contextlib.suppress(OSError):
136150
shutil.rmtree(path, ignore_errors=True)
151+
152+
153+
@contextlib.contextmanager
154+
def chdir(new_dir: PathType) -> t.Generator[None, None, None]:
155+
"""Context manager for changing the current working directory. This is not thread-safe."""
156+
prev_dir = os.getcwd()
157+
os.chdir(new_dir)
158+
try:
159+
yield
160+
finally:
161+
os.chdir(prev_dir)

src/bentoml/bentos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def build_bentofile(
413413
set_arguments(args)
414414
if bentofile:
415415
try:
416-
bentofile = resolve_user_filepath(bentofile, None)
416+
bentofile = resolve_user_filepath(bentofile, None, secure=False)
417417
except FileNotFoundError:
418418
raise InvalidArgument(f'bentofile "{bentofile}" not found')
419419
else:

src/bentoml_cli/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def pull(
301301

302302
if bentofile:
303303
try:
304-
bentofile = resolve_user_filepath(bentofile, None)
304+
bentofile = resolve_user_filepath(bentofile, None, secure=False)
305305
except FileNotFoundError:
306306
raise InvalidArgument(f'file "{bentofile}" not found')
307307
else:

src/bentoml_cli/secret.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def parse_kvs_argument_callback(
103103
if not key or not val:
104104
raise click.BadParameter(f"Invalid key-value pair: {key_val}")
105105
if val.startswith("@"):
106-
filename = resolve_user_filepath(val[1:], ctx=None)
106+
filename = resolve_user_filepath(val[1:], ctx=None, secure=False)
107107
if not os.path.exists(filename) or not os.path.isfile(filename):
108108
raise click.BadParameter(f"Invalid file path: {filename}")
109109
# read the file content
@@ -123,7 +123,7 @@ def read_dotenv_callback(
123123
env_map: dict[str, str] = {}
124124

125125
for path in value:
126-
path = resolve_user_filepath(path, ctx=None)
126+
path = resolve_user_filepath(path, ctx=None, secure=False)
127127
if not os.path.exists(path) or not os.path.isfile(path):
128128
raise click.BadParameter(f"Invalid file path: {path}")
129129
with open(path, "r") as f:

0 commit comments

Comments
 (0)