Skip to content

Commit e7c9949

Browse files
authored
Merge pull request #141 from ai-agent-assembly/v0.0.1/AAASM-3169/fix/adapter_validator_path_traversal
[AAASM-3169] 🔒 (python-sdk): Fix S8707 path-traversal in adapter_validator CLI
2 parents b1bab25 + 4a6002e commit e7c9949

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

agent_assembly/cli/adapter_validator.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,27 @@ def _check_unregister_hooks_idempotent(
173173

174174
def _check_entry_point_metadata(cls: type, path_or_module: str) -> AdapterValidationResult:
175175
"""Check entry point metadata in pyproject.toml if present at the given path."""
176-
search_path = Path(path_or_module)
176+
# Canonicalize the CLI-supplied path before any file access. resolve()
177+
# collapses ".." segments and symlinks, breaking path-traversal taint flows
178+
# (pythonsecurity:S8707): the validator only inspects the pyproject.toml that
179+
# sits directly inside the resolved adapter directory, never one reached by
180+
# escaping it.
181+
search_path = Path(path_or_module).resolve()
177182
if search_path.is_file():
178183
search_path = search_path.parent
179184

180-
pyproject_path = search_path / "pyproject.toml"
185+
pyproject_path = (search_path / "pyproject.toml").resolve()
186+
187+
# Containment guard: the resolved manifest must live directly under the
188+
# resolved adapter directory. A symlinked pyproject.toml pointing outside the
189+
# adapter root is rejected rather than read.
190+
if pyproject_path.parent != search_path:
191+
return AdapterValidationResult(
192+
check_name="entry_point_metadata",
193+
passed=False,
194+
message="pyproject.toml resolves outside the adapter directory; refusing to read it.",
195+
)
196+
181197
if not pyproject_path.is_file():
182198
return AdapterValidationResult(
183199
check_name="entry_point_metadata",

test/unit/cli/test_adapter_validator.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,45 @@ def test_no_pyproject_skips(self, valid_adapter_cls: type, tmp_path: object) ->
184184
assert result.passed is True
185185
assert "skipping" in result.message.lower()
186186

187+
def test_traversal_path_resolved_to_valid_dir(self, valid_adapter_cls: type, tmp_path: object) -> None:
188+
"""A path containing '..' that resolves to a real adapter dir still validates."""
189+
import pathlib
190+
191+
assert isinstance(tmp_path, pathlib.Path)
192+
adapter_dir = tmp_path / "adapter"
193+
adapter_dir.mkdir()
194+
pyproject = adapter_dir / "pyproject.toml"
195+
qualname = f"{valid_adapter_cls.__module__}:{valid_adapter_cls.__qualname__}"
196+
pyproject.write_text(f'[project.entry-points."agent_assembly.adapters"]\n' f'test_framework = "{qualname}"\n')
197+
# "<tmp>/adapter/../adapter" canonicalizes back to "<tmp>/adapter".
198+
traversal = str(adapter_dir / ".." / "adapter")
199+
result = _check_entry_point_metadata(valid_adapter_cls, traversal)
200+
assert result.passed is True
201+
202+
def test_symlinked_pyproject_outside_dir_rejected(self, valid_adapter_cls: type, tmp_path: object) -> None:
203+
"""A pyproject.toml symlink escaping the adapter dir is refused, not read."""
204+
import pathlib
205+
206+
assert isinstance(tmp_path, pathlib.Path)
207+
outside = tmp_path / "outside"
208+
outside.mkdir()
209+
secret = outside / "pyproject.toml"
210+
secret.write_text("[project]\nname = 'secret'\n")
211+
212+
adapter_dir = tmp_path / "adapter"
213+
adapter_dir.mkdir()
214+
link = adapter_dir / "pyproject.toml"
215+
try:
216+
link.symlink_to(secret)
217+
except (OSError, NotImplementedError):
218+
import pytest
219+
220+
pytest.skip("platform does not support symlinks")
221+
222+
result = _check_entry_point_metadata(valid_adapter_cls, str(adapter_dir))
223+
assert result.passed is False
224+
assert "outside the adapter directory" in result.message
225+
187226

188227
class TestValidateAdapter:
189228
"""Tests for validate_adapter orchestrator."""

0 commit comments

Comments
 (0)