From 3e17f3f9139ef56e519b4becc6224b23054ae1d3 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Thu, 18 Jun 2026 08:03:50 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=20(cli):=20Bound=20adapter=20p?= =?UTF-8?q?yproject=20path=20before=20file=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CLI-supplied path_or_module flowed into open() unvalidated, letting a crafted argument (.., absolute paths, symlinks) read arbitrary pyproject.toml files outside the adapter directory (pythonsecurity:S8707). resolve() the constructed manifest path and reject any pyproject.toml whose canonical parent is not the resolved adapter directory, breaking the taint flow and bounding traversal while preserving legitimate validation. Refs: AAASM-3169 Co-Authored-By: Claude Opus 4.8 (1M context) --- agent_assembly/cli/adapter_validator.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 3ca11747..0611a464 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -173,11 +173,27 @@ def _check_unregister_hooks_idempotent( def _check_entry_point_metadata(cls: type, path_or_module: str) -> AdapterValidationResult: """Check entry point metadata in pyproject.toml if present at the given path.""" - search_path = Path(path_or_module) + # Canonicalize the CLI-supplied path before any file access. resolve() + # collapses ".." segments and symlinks, breaking path-traversal taint flows + # (pythonsecurity:S8707): the validator only inspects the pyproject.toml that + # sits directly inside the resolved adapter directory, never one reached by + # escaping it. + search_path = Path(path_or_module).resolve() if search_path.is_file(): search_path = search_path.parent - pyproject_path = search_path / "pyproject.toml" + pyproject_path = (search_path / "pyproject.toml").resolve() + + # Containment guard: the resolved manifest must live directly under the + # resolved adapter directory. A symlinked pyproject.toml pointing outside the + # adapter root is rejected rather than read. + if pyproject_path.parent != search_path: + return AdapterValidationResult( + check_name="entry_point_metadata", + passed=False, + message="pyproject.toml resolves outside the adapter directory; refusing to read it.", + ) + if not pyproject_path.is_file(): return AdapterValidationResult( check_name="entry_point_metadata", From 4a6002ea0368ebf11947f9d95f8ecce3ae85825f Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Thu, 18 Jun 2026 08:03:58 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20(cli):=20Cover=20adapter=20path?= =?UTF-8?q?=20containment=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for the S8707 fix: a '..' path that canonicalizes back to a real adapter dir still validates, and a pyproject.toml symlink escaping the adapter directory is refused rather than read. Refs: AAASM-3169 Co-Authored-By: Claude Opus 4.8 (1M context) --- test/unit/cli/test_adapter_validator.py | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index ecb596f5..8a22bc17 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -184,6 +184,45 @@ def test_no_pyproject_skips(self, valid_adapter_cls: type, tmp_path: object) -> assert result.passed is True assert "skipping" in result.message.lower() + def test_traversal_path_resolved_to_valid_dir(self, valid_adapter_cls: type, tmp_path: object) -> None: + """A path containing '..' that resolves to a real adapter dir still validates.""" + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + adapter_dir = tmp_path / "adapter" + adapter_dir.mkdir() + pyproject = adapter_dir / "pyproject.toml" + qualname = f"{valid_adapter_cls.__module__}:{valid_adapter_cls.__qualname__}" + pyproject.write_text(f'[project.entry-points."agent_assembly.adapters"]\n' f'test_framework = "{qualname}"\n') + # "/adapter/../adapter" canonicalizes back to "/adapter". + traversal = str(adapter_dir / ".." / "adapter") + result = _check_entry_point_metadata(valid_adapter_cls, traversal) + assert result.passed is True + + def test_symlinked_pyproject_outside_dir_rejected(self, valid_adapter_cls: type, tmp_path: object) -> None: + """A pyproject.toml symlink escaping the adapter dir is refused, not read.""" + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + outside = tmp_path / "outside" + outside.mkdir() + secret = outside / "pyproject.toml" + secret.write_text("[project]\nname = 'secret'\n") + + adapter_dir = tmp_path / "adapter" + adapter_dir.mkdir() + link = adapter_dir / "pyproject.toml" + try: + link.symlink_to(secret) + except (OSError, NotImplementedError): + import pytest + + pytest.skip("platform does not support symlinks") + + result = _check_entry_point_metadata(valid_adapter_cls, str(adapter_dir)) + assert result.passed is False + assert "outside the adapter directory" in result.message + class TestValidateAdapter: """Tests for validate_adapter orchestrator."""