fix: audit hardening — input validation, error handling, and robustness (#25)

* fix: audit hardening — input validation, error handling, and robustness

- immich_api: guard person["id"] with .get() + early return on missing field
- immich_api: include page number in pagination exception log
- immich_api: validate faces response is a list before indexing
- executor: wrap Image.open() in try/except for non-image HTTP responses
- executor: strip leading 'v' from Frigate version before parsing (v0.16.0 was misread)
- config: wrap FRIGATE_SCORE_CEILING float() parse in try/except with warning
- config: warn when both DATA_DIR and legacy CWD config files exist simultaneously
- scheduler: wrap PID file write in try/except so /tmp failures don't crash startup
- scheduler: clamp sleep to 60s max to bound recovery time after NTP clock jumps
- frigate_api: log unexpected non-list type in get_frigate_person_files at DEBUG

* fix: LIMIT env var crash and symlink guard on person output dir

- jobs: wrap int(LIMIT) parse in try/except — bad value (e.g. "30.5", "all")
  now logs a warning and falls back to the default instead of crashing
- executor: check for symlink before shutil.rmtree on person_dir — prevents
  following a symlink out of OUTPUT_DIR on a shared volume

* chore: bump version to 0.5.3
This commit is contained in:
2026-06-14 19:39:35 -04:00
committed by GitHub
parent 166729a17d
commit 2e08504682
8 changed files with 88 additions and 14 deletions
+12 -2
View File
@@ -100,6 +100,9 @@ def execute_jobs(jobs: list[dict]) -> None:
logger.error(str(e))
continue
# Face crops are transient (uploaded then discarded); wipe before each run.
if os.path.islink(person_dir):
logger.error("person_dir %s is a symlink — refusing to remove", person_dir)
continue
if os.path.isdir(person_dir):
shutil.rmtree(person_dir)
os.makedirs(person_dir, exist_ok=True)
@@ -137,7 +140,14 @@ def execute_jobs(jobs: list[dict]) -> None:
headers=get_headers(),
timeout=30,
)
img = Image.open(BytesIO(resp.content)) if resp.ok else None
if resp.ok:
try:
img = Image.open(BytesIO(resp.content))
except Exception:
logger.warning("Invalid image data for asset %s", asset["id"])
img = None
else:
img = None
if img is None:
progress.console.print(f"[red]Failed download {asset['id']}[/red]")
@@ -208,7 +218,7 @@ def upload_to_frigate(jobs: list[dict]) -> None:
_frigate_version = get_frigate_version()
if _frigate_version is not None:
try:
parts = [int(x) for x in _frigate_version.split("-")[0].split(".") if x.isdigit()]
parts = [int(x) for x in _frigate_version.lstrip("v").split("-")[0].split(".") if x.isdigit()]
if len(parts) >= 2 and (parts[0], parts[1]) < (0, 16):
rprint(
f" [yellow]⚠ Frigate {_frigate_version} detected — "