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
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:
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
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")
|
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")
|
@router.get("/api/frames/{frame_id}/albums")
|
||||||
def api_albums(frame: Frame = Depends(require_frame_view)):
|
def api_albums(frame: Frame = Depends(require_frame_view)):
|
||||||
url, key = immich_creds(frame)
|
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:
|
if timezone is not None and timezone in quiet_hours.ALL_TIMEZONES:
|
||||||
cfg.timezone = timezone
|
cfg.timezone = timezone
|
||||||
if firmware_update_repo_url is not None:
|
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:
|
if firmware_auto_update is not None:
|
||||||
cfg.firmware_auto_update = firmware_auto_update
|
cfg.firmware_auto_update = firmware_auto_update
|
||||||
if battery_alert_threshold_pct is not None:
|
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}")
|
@router.get("/api/frames/{frame_id}/thumbnail/{asset_id}")
|
||||||
def api_thumbnail(asset_id: str, frame: Frame = Depends(require_frame_view)):
|
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)
|
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)
|
client = immich_client_for(frame)
|
||||||
try:
|
try:
|
||||||
content, content_type = client.download_asset_thumbnail(asset_id)
|
content, content_type = client.download_asset_thumbnail(asset_id)
|
||||||
|
|||||||
@@ -106,6 +106,9 @@
|
|||||||
<input type="checkbox" id="firmware_auto_update" {% if frame.firmware_auto_update %}checked{% endif %}>
|
<input type="checkbox" id="firmware_auto_update" {% if frame.firmware_auto_update %}checked{% endif %}>
|
||||||
<label for="firmware_auto_update">Automatically apply updates</label>
|
<label for="firmware_auto_update">Automatically apply updates</label>
|
||||||
</div>
|
</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>
|
<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>
|
<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>
|
<button type="button" class="secondary" id="firmware-check-now">Check now</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user