Procházet zdrojové kódy

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.
maziggy před 1 měsícem
rodič
revize
2a6df22075

+ 1 - 0
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.
 - **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.
 - **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.
 - **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
 ### 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.
 - **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
backend/app/api/routes/camera.py

@@ -2,6 +2,7 @@
 
 
 import asyncio
 import asyncio
 import logging
 import logging
+import os
 import subprocess
 import subprocess
 import sys
 import sys
 from collections.abc import AsyncGenerator
 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:
     try:
         success = await capture_camera_frame(
         success = await capture_camera_frame(

+ 5 - 2
backend/app/services/camera.py

@@ -7,6 +7,7 @@ Supports two camera protocols:
 
 
 import asyncio
 import asyncio
 import logging
 import logging
+import os
 import shutil
 import shutil
 import ssl
 import ssl
 import struct
 import struct
@@ -593,8 +594,10 @@ async def test_camera_connection(
     """
     """
     import tempfile
     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:
     try:
         success = await capture_camera_frame(
         success = await capture_camera_frame(

+ 5 - 2
backend/app/services/plate_detection.py

@@ -8,6 +8,7 @@ a reference image of the empty plate.
 from __future__ import annotations
 from __future__ import annotations
 
 
 import logging
 import logging
+import os
 from pathlib import Path
 from pathlib import Path
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -632,8 +633,10 @@ async def capture_camera_image(
 
 
             from backend.app.services.camera import capture_camera_frame
             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:
             try:
                 success = await capture_camera_frame(ip_address, access_code, model, tmp_path, timeout=10)
                 success = await capture_camera_frame(ip_address, access_code, model, tmp_path, timeout=10)