diff --git a/src/bentoml/_internal/cloud/deployment.py b/src/bentoml/_internal/cloud/deployment.py index c96aa86cbed..499578f5cc3 100644 --- a/src/bentoml/_internal/cloud/deployment.py +++ b/src/bentoml/_internal/cloud/deployment.py @@ -1627,9 +1627,17 @@ def _build_requirements_txt(bento_dir: str, image: Image | None) -> bytes: config = BentoBuildConfig.from_bento_dir(bento_dir) filename = config.python.requirements_txt content = b"" - if filename and os.path.exists(fullpath := os.path.join(bento_dir, filename)): - with open(fullpath, "rb") as f: - content = f.read().rstrip(b"\n") + b"\n" + if filename: + fullpath = os.path.join(bento_dir, filename) + if os.path.commonpath( + [os.path.realpath(fullpath), os.path.realpath(bento_dir)] + ) != os.path.realpath(bento_dir): + raise BentoMLException( + f"Path traversal detected: requirements.txt path '{filename}' is outside of bento directory '{bento_dir}'" + ) + if os.path.exists(fullpath): + with open(fullpath, "rb") as f: + content = f.read().rstrip(b"\n") + b"\n" elif config.python.packages: for package in config.python.packages: content += f"{package}\n".encode() @@ -1658,9 +1666,15 @@ def _build_post_setup_script(bento_dir: str, image: Image | None) -> bytes: config = BentoBuildConfig.from_bento_dir(bento_dir) if image and image.post_commands: content += "\n".join(image.post_commands).encode() + b"\n" - if config.docker.setup_script and os.path.exists( - fullpath := os.path.join(bento_dir, config.docker.setup_script) - ): - with open(fullpath, "rb") as f: - content += f.read() + if config.docker.setup_script: + fullpath = os.path.join(bento_dir, config.docker.setup_script) + if os.path.commonpath( + [os.path.realpath(fullpath), os.path.realpath(bento_dir)] + ) != os.path.realpath(bento_dir): + raise BentoMLException( + f"Path traversal detected: setup_script path '{config.docker.setup_script}' is outside of bento directory '{bento_dir}'" + ) + if os.path.exists(fullpath): + with open(fullpath, "rb") as f: + content += f.read() return content diff --git a/tests/unit/_internal/cloud/test_deployment.py b/tests/unit/_internal/cloud/test_deployment.py index 1e73c3b6fd9..3a2b2360772 100644 --- a/tests/unit/_internal/cloud/test_deployment.py +++ b/tests/unit/_internal/cloud/test_deployment.py @@ -537,3 +537,35 @@ def test_update_deployment_distributed(deployment_api: DeploymentAPI): deployment_strategy="RollingUpdate", ), } + + +def test_build_requirements_txt_path_traversal(tmp_path: t.Any): + from pathlib import Path + + from bentoml._internal.cloud.deployment import _build_requirements_txt + from bentoml.exceptions import BentoMLException + + bento_dir = Path(tmp_path) / "bento" + bento_dir.mkdir() + (bento_dir / "bentofile.yaml").write_text( + "service: service.py\npython:\n requirements_txt: ../../secret.txt\n" + ) + + with pytest.raises(BentoMLException, match="Path traversal detected"): + _build_requirements_txt(str(bento_dir), None) + + +def test_build_post_setup_script_path_traversal(tmp_path: t.Any): + from pathlib import Path + + from bentoml._internal.cloud.deployment import _build_post_setup_script + from bentoml.exceptions import BentoMLException + + bento_dir = Path(tmp_path) / "bento" + bento_dir.mkdir() + (bento_dir / "bentofile.yaml").write_text( + "service: service.py\ndocker:\n setup_script: ../../evil_setup.sh\n" + ) + + with pytest.raises(BentoMLException, match="Path traversal detected"): + _build_post_setup_script(str(bento_dir), None)