Skip to content

Commit 085c959

Browse files
committed
fix: address PR review comments
1 parent fd47b53 commit 085c959

File tree

3 files changed

+30
-5
lines changed

3 files changed

+30
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

88

9+
## [Unreleased]
10+
11+
912
## [1.14.0] - 2026-03-09
1013

1114
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ The CLI currently supports the following commands (and sub-commands):
4848
- `packages`: List packages for a repository. (Aliases `repos list`)
4949
- `repos`: List repositories for a namespace (owner).
5050
- `login`|`token`: Retrieve your API authentication token/key via login.
51+
- `logout`: Clear stored authentication credentials and SSO tokens (Keyring, API key from credential file and emit warning when `$CLOUDSMITH_API_KEY` is still set).
5152
- `metrics`: Metrics and statistics for a repository.
5253
- `tokens`: Retrieve bandwidth usage for entitlement tokens.
5354
- `packages`: Retrieve package usage for repository.

cloudsmith_cli/cli/commands/download.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def download( # noqa: C901
288288
for idx, file_info in enumerate(files_to_download, 1):
289289
filename = file_info["filename"]
290290
file_url = file_info["cdn_url"]
291-
output_path = os.path.join(output_dir, filename)
291+
output_path = _safe_join(output_dir, filename)
292292

293293
primary_marker = " (primary)" if file_info.get("is_primary") else ""
294294
tag = file_info.get("tag", "file")
@@ -408,7 +408,7 @@ def download( # noqa: C901
408408
if not outfile:
409409
# Extract filename from URL or use package name + format
410410
if package.get("filename"):
411-
outfile = package["filename"]
411+
outfile = os.path.basename(package["filename"])
412412
else:
413413
# Fallback to package name with extension based on format
414414
pkg_format = package.get("format", "bin")
@@ -552,7 +552,11 @@ def _download_all_packages( # noqa: C901
552552
)
553553

554554
if all_files:
555-
# Download all sub-files for this package
555+
# Download all sub-files for this package into a per-package subdir
556+
pkg_subdir = os.path.join(output_dir, f"{pkg_name}-{pkg_version}")
557+
if not os.path.exists(pkg_subdir):
558+
os.makedirs(pkg_subdir)
559+
556560
context_msg = f"Failed to get details for {pkg_name}!"
557561
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
558562
detail = get_package_detail(
@@ -563,7 +567,7 @@ def _download_all_packages( # noqa: C901
563567
for file_info in sub_files:
564568
filename = file_info["filename"]
565569
file_url = file_info["cdn_url"]
566-
file_path = os.path.join(output_dir, filename)
570+
file_path = _safe_join(pkg_subdir, filename)
567571
tag = file_info.get("tag", "file")
568572

569573
if not use_stderr:
@@ -612,7 +616,7 @@ def _download_all_packages( # noqa: C901
612616
# Download the primary package file
613617
download_url = pkg.get("cdn_url") or pkg.get("download_url")
614618
filename = pkg_filename or f"{pkg_name}-{pkg_version}"
615-
file_path = os.path.join(output_dir, filename)
619+
file_path = _safe_join(output_dir, filename)
616620

617621
if not download_url:
618622
# Fall back to detailed package info
@@ -725,6 +729,23 @@ def _download_all_packages( # noqa: C901
725729
)
726730

727731

732+
def _safe_join(base_dir, filename):
733+
"""Safely join base_dir and filename, preventing path traversal."""
734+
# Strip path separators and use only the basename
735+
safe_name = os.path.basename(filename)
736+
if not safe_name:
737+
raise click.ClickException(
738+
f"Invalid filename '{filename}' — cannot be empty after sanitization."
739+
)
740+
result = os.path.join(base_dir, safe_name)
741+
# Final check: resolved path must be under base_dir
742+
if not os.path.realpath(result).startswith(os.path.realpath(base_dir) + os.sep):
743+
raise click.ClickException(
744+
f"Filename '{filename}' resolves outside the target directory."
745+
)
746+
return result
747+
748+
728749
def _get_extension_for_format(pkg_format: str) -> str:
729750
"""Get appropriate file extension for package format."""
730751
format_extensions = {

0 commit comments

Comments
 (0)