Security fixes

auth.py

secrets.compare_digest replaces == for service key comparison (timing-safe)
Added require_service_key — bot-only endpoints (trip/event join/leave)
Added require_service_key_or_admin — node commands/config (bot via service key OR dashboard admin via Firebase)
Added _RateLimiter with three shared instances: trip_chat_limiter (20/5min per user), summarize_limiter (5/10min per incident), bootstrap_limiter (2/hr per system)
nodes.py

send_command and assign_system now require require_service_key_or_admin — the Discord bot can still call them via service key, but regular Firebase users are blocked
tokens.py

add_token, flush_tokens, set_preferred_system, delete_token all require require_admin_token
Token masking changed from token[:10] + "…" + token[-4:] to "•••" + token[-4:]
systems.py

All write endpoints (create, update, delete, ai-flags, ten-codes, vocabulary writes, bootstrap) now require require_admin_token
bootstrap_vocabulary also calls bootstrap_limiter.check(system_id)
incidents.py

POST /incidents/summarize (bulk) now requires require_admin_token
POST /incidents/{id}/summarize now calls summarize_limiter.check(incident_id)
trips.py

join_trip, leave_trip, join_event, leave_event require require_service_key — only the Discord bot can set Discord attendee identity
delete_trip, delete_event require require_service_key_or_admin
trip_chat rate-limited per caller UID, history stripped to user/assistant roles only, user message truncated to 2000 chars, Maps query strings capped at 200 chars
upload.py

Rejects files larger than settings.upload_max_bytes (default 100MB) with 413
storage.py

_safe_audio_filename() derives GCS object name from call_id + allowlisted extension, completely ignoring the client-supplied filename
config.py

Added upload_max_bytes: int = 100 * 1024 * 1024
Both Dockerfiles — python:3.14-slim → python:3.12-slim
This commit is contained in:
Logan
2026-06-21 13:40:08 -04:00
parent f0a0ea508a
commit 18d96193ab
11 changed files with 235 additions and 41 deletions
+7 -2
View File
@@ -4,7 +4,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query
from app.models import CommandPayload
from app.internal import firestore as fstore
from app.internal.mqtt_handler import mqtt_handler
from app.internal.auth import require_admin_token
from app.internal.auth import require_admin_token, require_service_key_or_admin
from app.routers.tokens import assign_token, release_token
router = APIRouter(prefix="/nodes", tags=["nodes"])
@@ -55,7 +55,11 @@ async def reject_node(node_id: str, _: dict = Depends(require_admin_token)):
@router.post("/{node_id}/command")
async def send_command(node_id: str, cmd: CommandPayload):
async def send_command(
node_id: str,
cmd: CommandPayload,
_: dict = Depends(require_service_key_or_admin),
):
node = await fstore.doc_get("nodes", node_id)
if not node:
raise HTTPException(404, f"Node '{node_id}' not found.")
@@ -108,6 +112,7 @@ async def assign_system(
system_id: str,
hardware_preset: str = Query("rtl-sdr-v3"),
ppm_override: Optional[float] = Query(None),
_: dict = Depends(require_service_key_or_admin),
):
"""
Assign a system to a node. Fetches the system config from Firestore