From 668f48897feea6032c736a7e019fccfbf8a8f587 Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Sun, 5 Jul 2026 07:43:07 +0000 Subject: [PATCH] fix: sanitize nasa_id and filenames to prevent path traversal (#1) - Add sanitize_filename() to strip path separators and parent refs - Apply sanitization to nasa_id used in metadata filenames - Apply sanitization to image filenames derived from asset_href - Preserve raw nasa_id for API calls to avoid breaking asset lookup --- downloader.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/downloader.py b/downloader.py index 6cdf2e3..c7e9b7d 100644 --- a/downloader.py +++ b/downloader.py @@ -44,6 +44,7 @@ import argparse import datetime import json import os +import re import sys import time from pathlib import Path @@ -180,6 +181,16 @@ def download_file(url: str, dest: Path) -> bool: raise RuntimeError(f"Download error for {url}: {e}") from e +def sanitize_filename(name: str) -> str: + """Sanitize a string for use as a filename, preventing path traversal.""" + # Strip path separators and parent directory references + sanitized = re.sub(r'[\/\\:\*\?"<>|]', "_", name) + sanitized = re.sub(r"^\.{1,2}($|_)", "item_", sanitized) + # Collapse multiple underscores + sanitized = re.sub(r"_+", "_", sanitized) + return sanitized.strip(".") or "item" + + def save_metadata(item: Dict, dest: Path) -> None: """Persist the full API item as formatted JSON.""" with dest.open("w", encoding="utf-8") as f: @@ -224,12 +235,13 @@ def main() -> None: for idx, item in enumerate(tqdm(items, desc="Downloading"), start=1): data = item.get("data", [{}])[0] - nasa_id = data.get("nasa_id") or data.get("title", f"item_{idx}") + raw_nasa_id = data.get("nasa_id") or data.get("title", f"item_{idx}") + nasa_id = sanitize_filename(str(raw_nasa_id)) # ------------------------------------------------------------------ # Asset lookup: fetch high‑resolution image URL # ------------------------------------------------------------------ - asset_url = f"{API_ASSET_URL}/{nasa_id}" + asset_url = f"{API_ASSET_URL}/{raw_nasa_id}" try: asset_resp = requests.get(asset_url, timeout=20) asset_resp.raise_for_status() @@ -243,7 +255,7 @@ def main() -> None: print(f"[{idx}] Failed asset lookup for {nasa_id}: {e}", file=sys.stderr) continue - filename = Path(asset_href).name + filename = sanitize_filename(Path(asset_href).name) image_path = IMG_DIR / filename meta_path = META_DIR / f"index-{nasa_id}.json"