security: 9 Findings aus Security-Audit behoben (CRITICAL + HIGH + MEDIUM)

CRITICAL:
- C-1: LDAP tls_verify Default False → True (MITM-Schutz)
- C-2: TOTP-Secret Fernet-verschlüsselt in DB (statt Plaintext)
  - core/crypto.py: encrypt_value() / decrypt_value() helper
  - Migration 0026: totp_secret VARCHAR(64→500), ldap tls_verify default=true
  - _totp_plain() helper mit Legacy-Fallback für bestehende Werte

HIGH:
- H-1: Kiosk Nonce-Cache asyncio.Lock (Race Condition behoben)
- H-2: File-Upload-Limit 10 MB (import_kimai.py + users.py CSV-Import)
- H-3: CORS allow_methods/allow_headers explizit eingeschränkt (war *)
- H-4: TrustedHostMiddleware aktiviert wenn ALLOWED_HOSTS gesetzt

MEDIUM:
- M-1: IP-Logging nutzt X-Forwarded-For hinter nginx-Proxy
- M-4: Audit-Log für password_changed, totp_enabled, totp_disabled
- M-5: CalDAV verify_ssl in Production erzwungen (_effective_verify_ssl)

152/152 Tests grün

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 19:45:09 +02:00
parent a639de13f8
commit 62c4e742ab
12 changed files with 319 additions and 31 deletions
+12 -1
View File
@@ -21,6 +21,17 @@ from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
from app.services.email_service import email_service
def _get_client_ip(request: "Request | None") -> str | None:
"""Gibt die echte Client-IP zurück (berücksichtigt X-Forwarded-For hinter nginx-Proxy)."""
if not request:
return None
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
# Erstes Element = Original-Client-IP (nginx setzt X-Forwarded-For)
return forwarded.split(",")[0].strip()
return request.client.host if request.client else None
def _slugify(name: str) -> str:
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
return slug[:80]
@@ -198,7 +209,7 @@ class AuthService:
refresh_token_hash=hashed_refresh,
expires_at=datetime.now(timezone.utc) + timedelta(days=settings.refresh_token_expire_days),
device=request.headers.get("User-Agent", "")[:255] if request else None,
ip=request.client.host if request and request.client else None,
ip=_get_client_ip(request),
)
db.add(session)
access_token = create_access_token(