From a791081ab19f56d6ac9b423a2bf5f08b2fdedcf6 Mon Sep 17 00:00:00 2001 From: Hiren Date: Wed, 25 Mar 2026 19:03:20 -0400 Subject: [PATCH 1/2] fix: reject path traversal in SSH URL parsing SSH URLs (git@host:owner/repo) bypassed the path validation that HTTPS URLs already apply. Paths like git@host:owner/../etc would parse without error. Apply the same traversal check to _parse_ssh_url: reject '.' and '..' segments, and reject empty segments from double slashes. Fixes #455 --- src/apm_cli/models/dependency/reference.py | 9 ++++++ tests/unit/test_path_security.py | 33 ++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/apm_cli/models/dependency/reference.py b/src/apm_cli/models/dependency/reference.py index 84a99202e..62e518446 100644 --- a/src/apm_cli/models/dependency/reference.py +++ b/src/apm_cli/models/dependency/reference.py @@ -623,6 +623,15 @@ def _parse_ssh_url(dependency_str: str): repo_part = repo_part[:-4] repo_url = repo_part.strip() + + # Reject path traversal sequences in SSH URLs + for segment in repo_url.split('/'): + if not segment or segment in ('.', '..'): + raise PathTraversalError( + f"Invalid SSH repository path '{repo_url}': " + f"path segments must not be '.' or '..'" + ) + return host, repo_url, reference, alias @classmethod diff --git a/tests/unit/test_path_security.py b/tests/unit/test_path_security.py index f93f95038..7e3888524 100644 --- a/tests/unit/test_path_security.py +++ b/tests/unit/test_path_security.py @@ -177,6 +177,39 @@ def test_parse_accepts_normal_virtual_package(self): dep = DependencyReference.parse("owner/repo/prompts/my-file.prompt.md") assert dep.is_virtual is True + # --- SSH URL traversal rejection --- + + def test_ssh_parse_rejects_dotdot_in_repo(self): + """SSH URLs with '..' traversal in the repo path must be rejected.""" + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:owner/../evil") + + def test_ssh_parse_rejects_nested_dotdot(self): + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:org/../../etc/passwd") + + def test_ssh_parse_rejects_single_dot(self): + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:owner/./repo") + + def test_ssh_parse_accepts_normal_url(self): + dep = DependencyReference.parse("git@github.com:owner/repo#main") + assert dep.repo_url == "owner/repo" + assert dep.reference == "main" + + def test_ssh_parse_accepts_url_with_git_suffix(self): + dep = DependencyReference.parse("git@gitlab.com:team/project.git#v1.0") + assert dep.repo_url == "team/project" + assert dep.reference == "v1.0" + + def test_ssh_parse_rejects_dotdot_with_alias(self): + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:owner/../evil@my-alias") + + def test_ssh_parse_rejects_dotdot_with_reference(self): + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:owner/../../etc#main") + # --------------------------------------------------------------------------- # DependencyReference.get_install_path containment From 8d61f63932312f0e38242cc079b1981e7cdaa44b Mon Sep 17 00:00:00 2001 From: Hiren Date: Wed, 25 Mar 2026 19:40:39 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20copilot=20review=20?= =?UTF-8?q?=E2=80=94=20update=20error=20message=20and=20add=20edge=20case?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the SSH traversal error message to mention empty segments, and add tests for double-slash and trailing-slash cases in SSH URLs. --- src/apm_cli/models/dependency/reference.py | 2 +- tests/unit/test_path_security.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/apm_cli/models/dependency/reference.py b/src/apm_cli/models/dependency/reference.py index 62e518446..71c3d581a 100644 --- a/src/apm_cli/models/dependency/reference.py +++ b/src/apm_cli/models/dependency/reference.py @@ -629,7 +629,7 @@ def _parse_ssh_url(dependency_str: str): if not segment or segment in ('.', '..'): raise PathTraversalError( f"Invalid SSH repository path '{repo_url}': " - f"path segments must not be '.' or '..'" + f"path segments must not be empty or be '.' or '..'" ) return host, repo_url, reference, alias diff --git a/tests/unit/test_path_security.py b/tests/unit/test_path_security.py index 7e3888524..76eb6d5ca 100644 --- a/tests/unit/test_path_security.py +++ b/tests/unit/test_path_security.py @@ -210,6 +210,14 @@ def test_ssh_parse_rejects_dotdot_with_reference(self): with pytest.raises(PathTraversalError): DependencyReference.parse("git@github.com:owner/../../etc#main") + def test_ssh_parse_rejects_double_slash(self): + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:owner//repo") + + def test_ssh_parse_rejects_trailing_slash(self): + with pytest.raises(PathTraversalError): + DependencyReference.parse("git@github.com:owner/repo/") + # --------------------------------------------------------------------------- # DependencyReference.get_install_path containment