diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7ccfa97..eae3a6d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to Bambuddy will be documented in this file. - **Unauthenticated Bug Report Endpoints** — The bug report endpoints (`/start-logging`, `/stop-logging`, `/submit`) had no authentication, allowing anyone on the network to enable debug logging, retrieve system logs, and trigger bug report submissions with system diagnostics when authentication was enabled. All three endpoints now require authentication — `start-logging` requires `settings:update` permission, `stop-logging` and `submit` require `settings:read`. Endpoints remain open when authentication is disabled (the default). Reported responsibly by Sacha Vaudey via security@bambuddy.cool. - **API Key Empty Printer List Grants Full Access** — An API key with an empty `printer_ids` list (`[]`) was treated identically to `null` (global access to all printers), granting full printer access instead of no access. Now `null` means global access (admin key) and `[]` means no printer access. Existing API keys with empty lists are automatically migrated to `null` on startup. Also fixed the webhook queue endpoint which used a falsy check that would bypass the filter for empty lists. Reported responsibly by Sacha Vaudey via security@bambuddy.cool. - **Missing HTTP Security Headers** — API responses did not include standard security headers. Added a middleware that sets `X-Content-Type-Options: nosniff` (prevents MIME-sniffing), `X-Frame-Options: DENY` (prevents clickjacking via iframe embedding), and `Referrer-Policy: strict-origin-when-cross-origin` (limits URL leakage to external services) on every response. `Content-Security-Policy` was omitted because the React SPA uses inline styles extensively and a permissive CSP would provide no meaningful protection. `Strict-Transport-Security` was omitted because Bambuddy is a LAN application commonly accessed over HTTP — HSTS would lock users out. Reported responsibly by Sacha Vaudey via security@bambuddy.cool. +- **Camera Snapshot Temp Files World-Readable** — Camera snapshot and plate detection endpoints created temporary JPEG files in `/tmp` with default 0644 permissions, making them readable by any local user. Switched from `NamedTemporaryFile(delete=False)` to `mkstemp` with explicit 0600 permissions so only the application user can read them. Cleanup was already handled via `finally` blocks. Reported responsibly by Sacha Vaudey via security@bambuddy.cool. ### Fixed - **Thumbnails Broken After Backend Restart** — Archive and library thumbnails returned 401 Unauthorized after a backend restart because stream tokens are stored in memory and lost on restart. The frontend now detects failed token-protected image loads and automatically refreshes the stream token, so thumbnails recover without a page reload. diff --git a/backend/app/api/routes/camera.py b/backend/app/api/routes/camera.py index de99abe2b..97453218e 100644 --- a/backend/app/api/routes/camera.py +++ b/backend/app/api/routes/camera.py @@ -2,6 +2,7 @@ import asyncio import logging +import os import subprocess import sys from collections.abc import AsyncGenerator @@ -769,9 +770,11 @@ async def camera_snapshot( }, ) - # Create temporary file for the snapshot - with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: - temp_path = Path(f.name) + # Create temporary file for the snapshot (0600 so only the app user can read it) + fd, tmp_name = tempfile.mkstemp(suffix=".jpg") + os.close(fd) + temp_path = Path(tmp_name) + temp_path.chmod(0o600) try: success = await capture_camera_frame( diff --git a/backend/app/services/camera.py b/backend/app/services/camera.py index 1a4a6dba7..a11361651 100644 --- a/backend/app/services/camera.py +++ b/backend/app/services/camera.py @@ -7,6 +7,7 @@ Supports two camera protocols: import asyncio import logging +import os import shutil import ssl import struct @@ -593,8 +594,10 @@ async def test_camera_connection( """ import tempfile - with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: - test_path = Path(f.name) + fd, tmp_name = tempfile.mkstemp(suffix=".jpg") + os.close(fd) + test_path = Path(tmp_name) + test_path.chmod(0o600) try: success = await capture_camera_frame( diff --git a/backend/app/services/plate_detection.py b/backend/app/services/plate_detection.py index 0ebff77bb..00652ffd5 100644 --- a/backend/app/services/plate_detection.py +++ b/backend/app/services/plate_detection.py @@ -8,6 +8,7 @@ a reference image of the empty plate. from __future__ import annotations import logging +import os from pathlib import Path logger = logging.getLogger(__name__) @@ -632,8 +633,10 @@ async def capture_camera_image( from backend.app.services.camera import capture_camera_frame - with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp: - tmp_path = Path(tmp.name) + fd, tmp_name = tempfile.mkstemp(suffix=".jpg") + os.close(fd) + tmp_path = Path(tmp_name) + tmp_path.chmod(0o600) try: success = await capture_camera_frame(ip_address, access_code, model, tmp_path, timeout=10)