Scope thumbnail access to the frame's own photos; validate Gitea repo URL
Build and push server image / build-and-push (push) Successful in 38s

Two fixes from a security pass over the server:

- /api/frames/{id}/thumbnail/{asset_id} accepted any asset id and
  fetched it via the frame owner's Immich credentials, unscoped to what
  that frame actually shows -- a user merely linked to view a frame
  could pull thumbnails for any asset in the owner's whole library, not
  just the frame's own album. Now scoped to current_asset_id/queue,
  matching the check device.frame_share and manage.manage_thumbnail
  already both apply.

- firmware_update_repo_url now has to be a plain http(s) URL. Unlike a
  one-off manual firmware upload (a deliberate, explicit act -- left
  alone), auto-update from a repo is a standing trust relationship: the
  frame keeps fetching from it and, with auto-update on, installs
  whatever it finds with nobody reviewing it first. Added a plain-
  language note next to the checkbox saying exactly that.
This commit is contained in:
2026-07-22 12:57:11 -04:00
parent dcbc71e683
commit 5b4fdbe330
2 changed files with 26 additions and 1 deletions
+23 -1
View File
@@ -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)
+3
View File
@@ -106,6 +106,9 @@
<input type="checkbox" id="firmware_auto_update" {% if frame.firmware_auto_update %}checked{% endif %}>
<label for="firmware_auto_update">Automatically apply updates</label>
</div>
<p class="sub" style="margin-top: 4px;">While on, this frame installs
whatever the repo above publishes next, with nobody reviewing it
first -- only point it at a repo you trust.</p>
<button type="button" class="secondary" id="firmware-settings-save">Save</button>
<p class="sub" id="firmware-gitea-status" style="display: none; margin-top: 10px;"></p>
<button type="button" class="secondary" id="firmware-check-now">Check now</button>