diff --git a/infographic/daytona-quote-sync/infographic.png b/infographic/daytona-quote-sync/infographic.png new file mode 100644 index 00000000000..b324606e56c Binary files /dev/null and b/infographic/daytona-quote-sync/infographic.png differ diff --git a/tests/tools/test_daytona_environment.py b/tests/tools/test_daytona_environment.py index 6f50bb7eb37..1081c06645c 100644 --- a/tests/tools/test_daytona_environment.py +++ b/tests/tools/test_daytona_environment.py @@ -413,3 +413,30 @@ class TestEnsureSandboxReady: env._sandbox.state = "started" env._ensure_sandbox_ready() env._sandbox.start.assert_not_called() + + +# --------------------------------------------------------------------------- +# Sync safety: shell-metacharacter quoting +# --------------------------------------------------------------------------- + +class TestSyncSafety: + def test_single_upload_quotes_parent_path(self, make_env, tmp_path): + """A remote path with shell metacharacters must be quoted, not injected.""" + env = make_env() + env._sandbox.process.exec.reset_mock() + + host_file = tmp_path / "token.txt" + host_file.write_text("secret", encoding="utf-8") + remote_path = "/root/.hermes/skills/evil; touch /tmp/daytona-owned/file.txt" + + env._daytona_upload(str(host_file), remote_path) + + mkdir_cmd = env._sandbox.process.exec.call_args_list[0][0][0] + # The whole parent dir is a single quoted argument — the ';' cannot + # break out into a second command. + assert mkdir_cmd == ( + "mkdir -p '/root/.hermes/skills/evil; touch /tmp/daytona-owned'" + ) + assert "; touch" not in mkdir_cmd.replace( + "'/root/.hermes/skills/evil; touch /tmp/daytona-owned'", "" + ) diff --git a/tools/environments/daytona.py b/tools/environments/daytona.py index 803cef1d90b..8aba7128041 100644 --- a/tools/environments/daytona.py +++ b/tools/environments/daytona.py @@ -154,7 +154,7 @@ class DaytonaEnvironment(BaseEnvironment): def _daytona_upload(self, host_path: str, remote_path: str) -> None: """Upload a single file via Daytona SDK.""" parent = str(Path(remote_path).parent) - self._sandbox.process.exec(f"mkdir -p {parent}") + self._sandbox.process.exec(quoted_mkdir_command([parent])) self._sandbox.fs.upload_file(host_path, remote_path) def _daytona_bulk_upload(self, files: list[tuple[str, str]]) -> None: