Restrict temp file permissions for camera snapshots

Camera snapshot, test, and plate detection endpoints created temporary
  JPEG files with default 0644 permissions. Switch from NamedTemporaryFile
  to mkstemp with explicit 0600 permissions.
This commit is contained in:
maziggy
2026-04-04 13:18:53 +02:00
parent 4d09b3f669
commit 2a6df22075
4 changed files with 17 additions and 7 deletions
+1
View File
@@ -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.
+6 -3
View File
@@ -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(
+5 -2
View File
@@ -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(
+5 -2
View File
@@ -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)