Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/agents/sandbox/session/manifest_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Comment on lines +105 to +107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize drive-relative child names before applying

On Windows, an ephemeral directory child named C:outside.txt bypasses this validation: Windows considers it drive-relative rather than absolute, and the join discards manifest_rel, while _validate_rel_path() sees neither an absolute POSIX path nor ... _prune_to_ephemeral() then preserves the raw key, and Dir.apply() uses native dest / Path(rel_dest), targeting the current directory on drive C instead of the workspace. Canonicalize child keys as POSIX paths before applying them or reject drive-relative Windows syntax, with a Windows-path regression test.

AGENTS.md reference: AGENTS.md:L125-L125

Useful? React with 👍 / 👎.

)
out.append((manifest_rel, self._prune_to_ephemeral(artifact)))
return
if isinstance(artifact, Dir):
Expand All @@ -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
Expand Down
31 changes: 30 additions & 1 deletion tests/sandbox/test_manifest_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down