Skip to content

Commit 9451997

Browse files
committed
fix(security): path traversal, shlex parsing, dead code cleanup
- Validate context_id against traversal (workspace.py) - Use is_relative_to instead of startswith (subagents.py) - Use shlex.split for interpreter/sources checks (permissions.py, executor.py) - Remove duplicate _MAX_SUB_AGENT_ITERATIONS (subagents.py) - Remove dead _BARE_DECISION_RE (reasoning.py)
1 parent 7a8e334 commit 9451997

5 files changed

Lines changed: 19 additions & 12 deletions

File tree

a2a/sandbox_agent/src/sandbox_agent/executor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,10 @@ def _check_sources(self, operation: str) -> str | None:
237237
"""
238238
import re
239239

240-
parts = operation.split()
240+
try:
241+
parts = shlex.split(operation)
242+
except ValueError:
243+
parts = operation.split()
241244
if not parts:
242245
return None
243246

a2a/sandbox_agent/src/sandbox_agent/permissions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import enum
2424
import fnmatch
2525
import re
26+
import shlex
2627
from typing import Any
2728

2829
# ---------------------------------------------------------------------------
@@ -280,7 +281,10 @@ def check_interpreter_bypass(cls, operation: str) -> list[str]:
280281
if not operation:
281282
return []
282283

283-
parts = operation.split()
284+
try:
285+
parts = shlex.split(operation)
286+
except ValueError:
287+
parts = operation.split()
284288
if not parts:
285289
return []
286290

a2a/sandbox_agent/src/sandbox_agent/reasoning.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,3 @@ def _parse_decision(content: str | list) -> str:
18131813
return decision
18141814

18151815
return "continue"
1816-
1817-
1818-
_BARE_DECISION_RE = re.compile(r"^(continue|retry|replan|done|hitl)\s*$", re.IGNORECASE)

a2a/sandbox_agent/src/sandbox_agent/subagents.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@
4040
_DELEGATION_MODES = os.environ.get("DELEGATION_MODES", "in-process,shared-pvc,isolated,sidecar").split(",")
4141
_DEFAULT_MODE = os.environ.get("DEFAULT_DELEGATION_MODE", "in-process")
4242

43-
# Maximum iterations for in-process sub-agents to prevent runaway loops.
44-
_MAX_SUB_AGENT_ITERATIONS = 15
45-
4643

4744
# ---------------------------------------------------------------------------
4845
# In-process sub-agent: explore (C20, mode 1)
@@ -109,7 +106,7 @@ async def read_file(path: str) -> str:
109106
File contents (truncated to 20000 chars).
110107
"""
111108
resolved = (ws_root / path).resolve()
112-
if not str(resolved).startswith(str(ws_root)):
109+
if not resolved.is_relative_to(ws_root):
113110
return "Error: path resolves outside the workspace."
114111
if not resolved.is_file():
115112
return f"Error: file not found at '{path}'."

a2a/sandbox_agent/src/sandbox_agent/workspace.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,15 @@ def __init__(
4444
# Public API
4545
# ------------------------------------------------------------------
4646

47+
@staticmethod
48+
def _validate_context_id(context_id: str) -> None:
49+
"""Reject context IDs that could escape the workspace root."""
50+
if not context_id or "/" in context_id or ".." in context_id or "\x00" in context_id:
51+
raise ValueError(f"Invalid context_id: {context_id!r}")
52+
4753
def get_workspace_path(self, context_id: str) -> str:
4854
"""Return the workspace path for *context_id* without creating it."""
55+
self._validate_context_id(context_id)
4956
return os.path.join(self.workspace_root, context_id)
5057

5158
def ensure_workspace(self, context_id: str) -> str:
@@ -60,10 +67,9 @@ def ensure_workspace(self, context_id: str) -> str:
6067
Raises
6168
------
6269
ValueError
63-
If *context_id* is empty.
70+
If *context_id* is empty or contains path-traversal characters.
6471
"""
65-
if not context_id:
66-
raise ValueError("context_id must not be empty")
72+
self._validate_context_id(context_id)
6773

6874
workspace_path = self.get_workspace_path(context_id)
6975
context_file = Path(workspace_path) / ".context.json"

0 commit comments

Comments
 (0)