security: H-1 settings-Whitelist + H-5 UUID-Guard + H-6 DNS-Pinning + H-7 Heartbeat-Timing
H-1: company.settings als typisiertes Sub-Schema - schemas/company.py: CompanySettingsUpdate mit extra=forbid - Nur bekannte Keys (carryover_expires_month/day) erlaubt - Unbekannte Keys → HTTP 422 H-5: SQL-Injection defensiv absichern - dependencies.py: UUID-Round-Trip str(_uuid.UUID(...)) + Sicherheitskommentar H-6: CalDAV DNS-Rebinding-Schutz - caldav_service.py: PinnedIPTransport — IP einmal auflösen, beim Request fixieren - _validate_caldav_url gibt aufgelöste IP zurück - Alle HTTP-Methoden nutzen PinnedIPTransport H-7: Heartbeat-Timestamp nach Route-Logik - kiosk_security.py: last_heartbeat_at-Update aus Dependency entfernt - kiosk_service.py: Update erst in process_heartbeat() nach erfolgreicher Auth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import uuid as _uuid
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
@@ -60,10 +61,13 @@ async def get_current_user(
|
||||
# bypass so subsequent queries in the same transaction are automatically
|
||||
# filtered to the user's company.
|
||||
if user.role != UserRole.SUPER_ADMIN and user.company_id is not None:
|
||||
# SET LOCAL does not accept bind parameters in PostgreSQL; the value
|
||||
# must be inlined. We sanitise by converting through uuid.UUID first
|
||||
# so an attacker-supplied token payload cannot inject arbitrary SQL.
|
||||
safe_company_id = str(user.company_id) # already a UUID object from db.get()
|
||||
# Sicherheits-Invariante: safe_company_id muss eine valide UUID sein.
|
||||
# Der _uuid.UUID()-Round-Trip verhindert SQL-Injection auch bei zukünftigen
|
||||
# Refactorings (z.B. falls user.company_id einmal ein String aus einem
|
||||
# anderen Pfad käme). SET LOCAL akzeptiert keine Bind-Parameter in
|
||||
# PostgreSQL, daher ist String-Interpolation hier unvermeidlich —
|
||||
# der UUID-Round-Trip ist die kryptographische Absicherung dagegen.
|
||||
safe_company_id = str(_uuid.UUID(str(user.company_id)))
|
||||
await db.execute(text(f"SET LOCAL app.company_id = '{safe_company_id}'"))
|
||||
await db.execute(text("SET LOCAL app.bypass_rls = 'off'"))
|
||||
|
||||
|
||||
@@ -177,9 +177,10 @@ async def verify_kiosk_request(
|
||||
3. Gerät laden + Status prüfen
|
||||
4. IP-Whitelist (falls konfiguriert)
|
||||
5. Ed25519-Signatur verifizieren
|
||||
6. last_heartbeat_at aktualisieren
|
||||
|
||||
Gibt das verifizierte KioskDevice zurück.
|
||||
Hinweis: last_heartbeat_at wird NICHT hier gesetzt (H-7) – nur der Heartbeat-
|
||||
Endpoint setzt es, nach erfolgreicher Route-Logik.
|
||||
"""
|
||||
# 1. Timestamp-Check
|
||||
try:
|
||||
@@ -259,8 +260,10 @@ async def verify_kiosk_request(
|
||||
except InvalidSignature:
|
||||
raise HTTPException(status_code=401, detail="Ungültige Signatur.")
|
||||
|
||||
# 6. Heartbeat-Zeitstempel aktualisieren
|
||||
device.last_heartbeat_at = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
# Hinweis: last_heartbeat_at wird NICHT hier gesetzt, sondern erst im
|
||||
# Heartbeat-Route-Handler (process_heartbeat). So wird der Timestamp nur
|
||||
# committed wenn die gesamte Route-Logik erfolgreich war, nicht bereits
|
||||
# bei jedem signierten Request. Andere Endpunkte (auth/pin etc.) aktualisieren
|
||||
# last_heartbeat_at bewusst nicht – nur echter Heartbeat zählt als Liveness-Signal.
|
||||
|
||||
return device
|
||||
|
||||
Reference in New Issue
Block a user