diff --git a/src/agents/sandbox/session/manifest_application.py b/src/agents/sandbox/session/manifest_application.py index bb3569a9fa..e6ba7eb2b9 100644 --- a/src/agents/sandbox/session/manifest_application.py +++ b/src/agents/sandbox/session/manifest_application.py @@ -100,6 +100,12 @@ def _collect_ephemeral_entries( manifest_rel = Manifest._coerce_rel_path(rel_dest) Manifest._validate_rel_path(manifest_rel) if artifact.ephemeral: + if isinstance(artifact, Dir): + for child_name in artifact.children: + self._validate_ephemeral_child_paths( + manifest_rel / Manifest._coerce_rel_path(child_name), + artifact.children[child_name], + ) out.append((manifest_rel, self._prune_to_ephemeral(artifact))) return if isinstance(artifact, Dir): @@ -110,6 +116,14 @@ def _collect_ephemeral_entries( out=out, ) + def _validate_ephemeral_child_paths(self, rel_dest: Path, artifact: BaseEntry) -> None: + Manifest._validate_rel_path(rel_dest) + if isinstance(artifact, Dir): + for child_name, child_artifact in artifact.children.items(): + self._validate_ephemeral_child_paths( + rel_dest / Manifest._coerce_rel_path(child_name), child_artifact + ) + def _prune_to_ephemeral(self, artifact: BaseEntry) -> BaseEntry: if not isinstance(artifact, Dir): return artifact diff --git a/tests/sandbox/test_manifest_application.py b/tests/sandbox/test_manifest_application.py index d8be0bd31e..a8f301e571 100644 --- a/tests/sandbox/test_manifest_application.py +++ b/tests/sandbox/test_manifest_application.py @@ -14,7 +14,7 @@ InContainerMountStrategy, MountpointMountPattern, ) -from agents.sandbox.errors import ExecNonZeroError +from agents.sandbox.errors import ExecNonZeroError, InvalidManifestPathError from agents.sandbox.manifest import Manifest from agents.sandbox.materialization import MaterializedFile from agents.sandbox.session.manifest_application import ManifestApplier @@ -25,6 +25,35 @@ def _materialized(dest: Path) -> list[MaterializedFile]: return [MaterializedFile(path=dest, sha256=dest.as_posix())] +@pytest.mark.asyncio +async def test_manifest_applier_rejects_unsafe_children_of_ephemeral_directory() -> None: + async def mkdir(_path: Path) -> None: + return None + + async def exec_checked_nonzero(*_command: str) -> ExecResult: + return ExecResult(stdout=b"", stderr=b"", exit_code=0) + + async def apply_entry(_entry: object, _dest: Path, _base_dir: Path) -> list[MaterializedFile]: + return [] + + applier = ManifestApplier( + mkdir=mkdir, + exec_checked_nonzero=exec_checked_nonzero, + apply_entry=apply_entry, + ) + manifest = Manifest( + entries={ + "safe": Dir( + ephemeral=True, + children={"../outside.txt": File(content=b"nope")}, + ) + } + ) + + with pytest.raises(InvalidManifestPathError, match="must not escape root"): + await applier.apply_manifest(manifest, only_ephemeral=True) + + @pytest.mark.asyncio async def test_manifest_applier_only_applies_ephemeral_entries_without_account_provisioning() -> ( None