Skip to content

Commit f08eadc

Browse files
committed
fix: keep local environment file access in workspace
1 parent 73ecf8d commit f08eadc

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

src/google/adk/environment/_local_environment.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,16 @@ async def write_file(self, path: str | Path, content: str | bytes) -> None:
142142
return await asyncio.to_thread(self._sync_write, resolved, content)
143143

144144
def _resolve_path(self, path: str | Path) -> Path:
145-
"""Resolve a relative path against the working directory."""
146-
path_obj = Path(path)
147-
if path_obj.is_absolute():
148-
return path_obj
149-
return self.working_dir / path_obj
145+
"""Resolve a file path inside the working directory."""
146+
candidate = Path(path)
147+
working_dir = self.working_dir.resolve()
148+
if not candidate.is_absolute():
149+
candidate = working_dir / candidate
150+
151+
resolved = candidate.resolve()
152+
if not resolved.is_relative_to(working_dir):
153+
raise ValueError(f'Path escapes working directory: {path}')
154+
return resolved
150155

151156
@staticmethod
152157
def _sync_read(path: Path) -> bytes:

tests/unittests/tools/test_local_environment.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,39 @@ async def test_write_creates_parent_dirs(self, env: LocalEnvironment):
7676
data = await env.read_file("sub/dir/file.txt")
7777
assert data == b"nested"
7878

79+
@pytest.mark.asyncio
80+
async def test_absolute_path_inside_working_dir(self, env: LocalEnvironment):
81+
"""Absolute paths are accepted when they stay inside the workspace."""
82+
path = env.working_dir / "absolute.txt"
83+
await env.write_file(path, "absolute")
84+
data = await env.read_file(path)
85+
assert data == b"absolute"
86+
87+
@pytest.mark.asyncio
88+
async def test_rejects_relative_path_escape(self, env: LocalEnvironment):
89+
"""Parent traversal cannot escape the workspace."""
90+
outside = env.working_dir.parent / "outside.txt"
91+
outside.write_text("secret", encoding="utf-8")
92+
93+
with pytest.raises(ValueError, match="escapes working directory"):
94+
await env.read_file(Path("..") / outside.name)
95+
96+
with pytest.raises(ValueError, match="escapes working directory"):
97+
await env.write_file(Path("..") / "write-outside.txt", "nope")
98+
99+
assert not (env.working_dir.parent / "write-outside.txt").exists()
100+
101+
@pytest.mark.asyncio
102+
async def test_rejects_absolute_path_outside_working_dir(
103+
self, env: LocalEnvironment
104+
):
105+
"""Absolute paths outside the workspace are rejected."""
106+
outside = env.working_dir.parent / "outside-absolute.txt"
107+
outside.write_text("secret", encoding="utf-8")
108+
109+
with pytest.raises(ValueError, match="escapes working directory"):
110+
await env.read_file(outside)
111+
79112
@pytest.mark.asyncio
80113
async def test_read_nonexistent_raises(self, env: LocalEnvironment):
81114
"""Reading a missing file raises FileNotFoundError."""

0 commit comments

Comments
 (0)