diff --git a/server/app/routers/api_frames.py b/server/app/routers/api_frames.py index 9ac16cf..fc60f27 100644 --- a/server/app/routers/api_frames.py +++ b/server/app/routers/api_frames.py @@ -17,6 +17,7 @@ from __future__ import annotations import logging import time +from urllib.parse import urlparse import httpx from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile @@ -59,6 +60,17 @@ MAX_QUEUE_TARGET_LEN = 5000 ORIENTATIONS = ("landscape", "portrait", "landscape_flipped", "portrait_flipped") +def _valid_repo_url(url: str) -> bool: + """The frame will periodically fetch from this URL on its own (see + gitea_releases.py) and, with auto-update on, install whatever it + finds -- unlike a one-off manual firmware upload, that's a standing + trust relationship, so it's worth rejecting obviously-wrong input at + save time rather than only failing later at fetch time. http(s) only + -- no file://, no other schemes.""" + parsed = urlparse(url) + return parsed.scheme in ("http", "https") and bool(parsed.netloc) + + @router.get("/api/frames/{frame_id}/albums") def api_albums(frame: Frame = Depends(require_frame_view)): url, key = immich_creds(frame) @@ -129,7 +141,10 @@ def api_config_save( if timezone is not None and timezone in quiet_hours.ALL_TIMEZONES: cfg.timezone = timezone if firmware_update_repo_url is not None: - cfg.firmware_update_repo_url = firmware_update_repo_url.strip() + stripped = firmware_update_repo_url.strip() + if stripped and not _valid_repo_url(stripped): + raise HTTPException(400, "Firmware repo URL must be a plain http:// or https:// URL") + cfg.firmware_update_repo_url = stripped if firmware_auto_update is not None: cfg.firmware_auto_update = firmware_auto_update if battery_alert_threshold_pct is not None: @@ -321,7 +336,14 @@ def api_queue_remove( @router.get("/api/frames/{frame_id}/thumbnail/{asset_id}") def api_thumbnail(asset_id: str, frame: Frame = Depends(require_frame_view)): + """Scoped to what this frame is actually showing/queuing -- a user + merely linked to view this frame shouldn't be able to pull thumbnails + for arbitrary asset ids in the owner's Immich library, only the + frame's own curated album. Same rule device.frame_share and + manage.manage_thumbnail already enforce.""" require_configured(frame) + if asset_id != frame.current_asset_id and asset_id not in frame.queue: + raise HTTPException(404, "Not on this frame") client = immich_client_for(frame) try: content, content_type = client.download_asset_thumbnail(asset_id) diff --git a/server/app/templates/frame_config.html b/server/app/templates/frame_config.html index acee31b..1be3465 100644 --- a/server/app/templates/frame_config.html +++ b/server/app/templates/frame_config.html @@ -106,6 +106,9 @@ +

While on, this frame installs + whatever the repo above publishes next, with nobody reviewing it + first -- only point it at a repo you trust.