Skip to content

Commit 757b5ad

Browse files
fix: reject path traversal in SSH URL parsing (#458)
* 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 * fix: address copilot review — update error message and add edge case tests Update the SSH traversal error message to mention empty segments, and add tests for double-slash and trailing-slash cases in SSH URLs. --------- Co-authored-by: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com>
1 parent fdd0003 commit 757b5ad

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

src/apm_cli/models/dependency/reference.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,15 @@ def _parse_ssh_url(dependency_str: str):
655655
repo_part = repo_part[:-4]
656656

657657
repo_url = repo_part.strip()
658+
659+
# Reject path traversal sequences in SSH URLs
660+
for segment in repo_url.split('/'):
661+
if not segment or segment in ('.', '..'):
662+
raise PathTraversalError(
663+
f"Invalid SSH repository path '{repo_url}': "
664+
f"path segments must not be empty or be '.' or '..'"
665+
)
666+
658667
return host, repo_url, reference, alias
659668

660669
@classmethod

tests/unit/test_path_security.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,47 @@ def test_parse_accepts_normal_virtual_package(self):
177177
dep = DependencyReference.parse("owner/repo/prompts/my-file.prompt.md")
178178
assert dep.is_virtual is True
179179

180+
# --- SSH URL traversal rejection ---
181+
182+
def test_ssh_parse_rejects_dotdot_in_repo(self):
183+
"""SSH URLs with '..' traversal in the repo path must be rejected."""
184+
with pytest.raises(PathTraversalError):
185+
DependencyReference.parse("git@github.com:owner/../evil")
186+
187+
def test_ssh_parse_rejects_nested_dotdot(self):
188+
with pytest.raises(PathTraversalError):
189+
DependencyReference.parse("git@github.com:org/../../etc/passwd")
190+
191+
def test_ssh_parse_rejects_single_dot(self):
192+
with pytest.raises(PathTraversalError):
193+
DependencyReference.parse("git@github.com:owner/./repo")
194+
195+
def test_ssh_parse_accepts_normal_url(self):
196+
dep = DependencyReference.parse("git@github.com:owner/repo#main")
197+
assert dep.repo_url == "owner/repo"
198+
assert dep.reference == "main"
199+
200+
def test_ssh_parse_accepts_url_with_git_suffix(self):
201+
dep = DependencyReference.parse("git@gitlab.com:team/project.git#v1.0")
202+
assert dep.repo_url == "team/project"
203+
assert dep.reference == "v1.0"
204+
205+
def test_ssh_parse_rejects_dotdot_with_alias(self):
206+
with pytest.raises(PathTraversalError):
207+
DependencyReference.parse("git@github.com:owner/../evil@my-alias")
208+
209+
def test_ssh_parse_rejects_dotdot_with_reference(self):
210+
with pytest.raises(PathTraversalError):
211+
DependencyReference.parse("git@github.com:owner/../../etc#main")
212+
213+
def test_ssh_parse_rejects_double_slash(self):
214+
with pytest.raises(PathTraversalError):
215+
DependencyReference.parse("git@github.com:owner//repo")
216+
217+
def test_ssh_parse_rejects_trailing_slash(self):
218+
with pytest.raises(PathTraversalError):
219+
DependencyReference.parse("git@github.com:owner/repo/")
220+
180221

181222
# ---------------------------------------------------------------------------
182223
# DependencyReference.get_install_path containment

0 commit comments

Comments
 (0)