leistungsbilanz-ts/docs/THREATMODEL.md
Grovy311 e51752f3e1
Some checks are pending
security-baseline / secrets (push) Waiting to run
security-baseline / node (push) Waiting to run
security-baseline / semgrep (push) Waiting to run
security-baseline / trivy (push) Waiting to run
docs(security): Design-Entscheidung D1 (Projekte teamweit geteilt) eintragen
Julian hat am 17.10.2026 bestaetigt: die Software laeuft nur im eigenen
Team, alle Nutzer sehen alle Projekte. Bevor Externe Konten bekommen, wird
Besitz/Freigabe pro Projekt eingefuehrt. R3 (ASVS-light B1/B2) damit nicht
anwendbar; idorGap-Kommentar und AGENTS.md verweisen auf D1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:56:23 +02:00

12 KiB

Threat-Model (light): leistungsbilanz-ts

Stand: 17.10.2026 (Branch security-baseline). Risiken mit Maßnahme und prüfbarem Beleg. Prioritäten wie ASVS-light: K blockiert den Release, H/M werden Auflage oder Ausnahme (docs/RELEASE-AUSNAHMEN.md). Ein Risiko ohne Beleg ist offen.

System

  • Browser → Caddy (TLS, leistungsbilanz.jansseneconsult.de, leistungsbilanz.electric-consult.de, ohne Authelia) → Web (Next.js, Container-Port 3001, Host 0.0.0.0:3090) → Next-Rewrite → API (Express 5, Port 3000, nur im Compose-Netz) → SQLite data/leistungsbilanz.db
  • Browser im LAN → http://192.168.0.133:3090 direkt, ohne TLS und am Caddy vorbei
  • Anmeldung nur in der App: POST /api/auth/login → Session-Cookie lb_session (httpOnly, SameSite=Lax, Secure per COOKIE_SECURE), Sessions im Speicher der API
  • Schreibzugriffe: zusätzlich Header X-Leistungsbilanz-Request: 1, Sec-Fetch-Site wird geprüft
  • Rollen: admin (zusätzlich /api/users/*) und user (alles andere), kein Self-Signup
  • Fremdinhalt: Revit/Linear-XLSX (Base64 im JSON, /api/projects/:id/... Erst- und Folgeimport), Projektdatei *.leistungsbilanz.json (POST /api/projects/import, /:projectId/import), Dokument-Logo (PUT /api/document-branding/logo)
  • Ausgaben: Projekt-Export als JSON (GET /api/projects/:id/export), Rückgabe-XLSX für Linear, Druckansicht/PDF über den Browser

Live-Instanz auf cj-ki: /home/cj/leistungsbilanz (Branch deploy/cj-ki, Stand 28.09.2026, ohne diese Baseline). Der Caddy-Kommentar dort hält fest: bewusst ohne Authelia seit 28.09.2026, weil die App eine eigene Anmeldung hat.

Assets

Asset Schutzbedarf Bemerkung
Projektdaten (Verteiler, Stromkreise, Lasten, Räume, Grobbilanz) hoch Planungsdaten von Kundenprojekten (Daten Dritter). Fehler wirken auf die Auslegung der Anlage.
Importierte Revit/Linear-XLSX inkl. Originalbytes des letzten Imports hoch Kundendaten, liegen in der DB und im Projekt-Export
Projektverlauf, Snapshots, Undo/Redo mittel Wiederherstellbarkeit nach Fehlbedienung
Benutzerkonten (Anmeldename, Name, Rolle, scrypt-Hash) hoch personenbezogen, Zugang zu allen Projekten (siehe R3)
Session-Cookie lb_session hoch wer es hat, ist der Nutzer
Büroprofil und Logo für Druckköpfe mittel erscheint auf allen Ausdrucken
Bootstrap-Admin-Passwort in .env hoch nur bis zum ersten Start relevant
Echte Beispieldatei docs/spec/kbp-Stromkreis.xlsx hoch gitignored, nie in Tests oder Commits

Gesundheitsdaten: keine. Personenbezug: nur Benutzerkonten.

Angreifer

  • A1 anonym aus dem Internet (Instanz ist über Caddy öffentlich erreichbar)
  • A2 jemand im LAN, direkt an Port 3090 (HTTP, am Caddy vorbei)
  • A3 angemeldeter Nutzer mit Rolle user, der Admin-Rechte will
  • A4 Kollege mit gleicher Rolle, der fremde Projekte liest oder ändert (IDOR)
  • A5 präparierte Importdatei (XLSX, Projektdatei, Logo), auch ungewollt von Kunden weitergereicht
  • A6 kompromittiertes npm-Paket, KI-Agent mit Schreibrechten im Repo

Vertrauensgrenzen

  1. Internet/LAN → Web: Identität wird nur in der API über lb_session festgestellt. Kein Proxy-Header wird geglaubt (gefälschtes X-Remote-User wird im Baseline-Test abgelehnt).
  2. Web → API: die API ist nicht auf dem Host veröffentlicht (expose), der Next-Rewrite reicht Cookie und Header durch.
  3. API → Projektdaten: nach dem Login gibt es keine Prüfung pro Projekt (R3).
  4. Importdateien sind Fremdinhalt und werden erst nach dem Auth-Guard verarbeitet.

Risiken

# Risiko Angreifer Wirkung Prio Maßnahme Beleg Status
R1 Zugriff ohne Anmeldung A1, A2 alle Projekte lesen/ändern K Auth-Guard vor allen /api-Routen, offen nur login/status/logout und /health; Passwortwechsel-Pflicht vor allem anderen tests/security/baseline.test.ts „jede Route verlangt Login“, „gefälschte Identität wird abgelehnt“; roles.test.ts „ohne Passwortwechsel …“; Mutationen „Login-Pflicht entfernt“, „ungeschützte Route vor dem Auth-Guard“, „Passwortwechsel-Pflicht entfernt“ belegt
R2 Rechteausweitung user → admin, Benutzerverwaltung A3 Konten anlegen, Passwörter zurücksetzen, Admin werden K /api/users/* nur admin; Rolle kommt aus der DB, nie aus der Anfrage; zod .strict(); letzter aktiver Admin nicht entfernbar (atomar); Reset/Deaktivieren beendet alle Sessions roles.test.ts „normaler Nutzer bekommt 403 …“ + Gegenprobe; authentication.test.ts „never removes the last active admin“, „… concurrent updates“; Baseline Mass Assignment (role: "admin"); Mutation „Admin-Rolle für /api/users entfernt“ belegt
R3 Fremde Projekte lesen, ändern, exportieren, löschen (ASVS-light B1/B2: Objektzugriff, Listen) A4 jeder angemeldete Nutzer hat Vollzugriff auf alle Projekte K keine Prüfung pro Projekt, Projekte haben keinen Besitzer: Design-Entscheidung D1 (teamweit geteilt, Konten nur für das eigene Team) D1 unten; IDOR-/Listen-Tests in baseline.test.ts laufen und zählen per idorGap (tests/security/security.config.ts) als TODO nicht anwendbar (D1), neu bewerten bei externen Konten
R4 CSRF von fremden Seiten oder anderen Diensten A1, A2 Änderungen im Namen eines angemeldeten Nutzers H SameSite=Lax, Pflichtheader X-Leistungsbilanz-Request: 1 für POST/PUT/PATCH/DELETE (erzwingt CORS-Preflight), Sec-Fetch-Site wenn vorhanden, keine CORS-Freigabe authentication.test.ts „rejects writes without the application header …“; Baseline „CORS lässt fremde Origins nicht zu“; Mutationen „CSRF-Header-Prüfung entfernt“, „CORS für alle Origins“ belegt
R5 Passwort-Raten und Session-Diebstahl über das Internet A1 Konto übernommen, damit alle Projekte (R3) H scrypt mit Salt, 5 Fehlversuche je Name in 15 min → 429, gleiche Laufzeit für unbekannte Namen, Längengrenzen vor scrypt, neue Session-ID bei Login und Passwortwechsel, gleitende 12 h + harte 7 Tage, max. 20 Sessions je Nutzer. Bewusst nicht: dauerhafte Sperre, IP-basierte Drosselung (alle Anfragen kommen über den Rewrite mit derselben IP), 2. Faktor authentication.test.ts (Hash, Throttle, Session-Laufzeit, Look-alike-Namen, Längengrenzen) belegt; Restrisiko öffentliche Login-Seite siehe E2
R6 Klartext im LAN: Port 3090 auf 0.0.0.0, HTTP am Caddy vorbei; Passwort und lb_session mitlesbar; über http://192.168.0.133:3090 gesetzte Cookies gehen an alle HTTP-Dienste dieses Hosts A2 Session-Übernahme H Port nur für Caddy erreichbar machen (Bind/Firewall), Zugriff nur über die TLS-Hostnamen, COOKIE_SECURE=true /home/cj/leistungsbilanz/compose.local.yaml (${LB_WEB_PORT:-3090}:3001); Wert von COOKIE_SECURE in der Live-.env nicht geprüft offen
R7 Großer JSON-Body vor der Anmeldung: express.json({ limit: "25mb" }) gilt global vor dem Auth-Guard, also auch für POST /api/auth/login A1 Speicher-DoS (API-Container 512 MB, Neustart beendet alle Sessions) M kleines globales Limit, 25 MB nur auf den Import-Routen nach dem Auth-Guard (wie netzberechnung R5) src/server/app.ts:36; kein Test offen
R8 Präparierte XLSX: übergroß, Zip-Bombe, Formeln, Datei zwischen Vorschau und Übernahme getauscht A5 Speicher-DoS, falsche Daten im Projekt M Endung .xlsx, max. 10 MB, kanonisches Base64, nach dem Lesen max. 50.000 Zeilen / 256 Spalten / 2 Mio. Zellen; Formeln werden nie ausgewertet (nur zwischengespeicherter Wert, sonst Warnung); Übernahme nur bei gleicher SHA-256 wie die Vorschau und passender Projektrevision. Offen: ExcelJS entpackt die ganze Datei, bevor die Zeilen-/Zellgrenzen greifen (Zip-Bombe innerhalb 10 MB) tests/external-xlsx-transport.test.ts „enforces extension, byte size and post-read sheet limits“; revit-xlsx-scale.test.ts; external-xlsx-initial-import.test.ts; kein Zip-Bomben-Test teilweise, Zip-Bombe offen
R9 Präparierte Projektdatei (*.leistungsbilanz.json, Snapshot-Restore) A5 kaputter oder untergeschobener Projektstand M zod-Schemas (project-transfer.schemas.ts), Schemaversion, Prüfsumme und Zustands-Hash, Import als neues Projekt oder als Revision mit Undo. Prüfsumme sichert Integrität, nicht Herkunft: wer die Datei baut, kann sie korrekt berechnen tests/project-state-snapshot.test.ts; Baseline „ungültige Eingaben führen nie zu 5xx“ belegt (Integrität), Herkunft bewusst nicht
R10 Export: Projekt-JSON enthält alle Daten inkl. XLSX-Originalbytes; Header-Injection über den Projektnamen im Dateinamen A4 Abfluss von Kundendaten M safeFilename (nur a-zA-Z0-9_-, max. 80 Zeichen); Zugriff wie R3 src/server/controllers/project-transfer.controller.ts:120; Export ist in idorCases (R3) Dateiname belegt durch Code, kein Test; Zugriff siehe R3
R11 Logo-Upload: Skript im Bild (SVG), falscher Typ, jeder user darf Logo und Büroprofil für alle Ausdrucke ändern A4, A5 XSS beim direkten Öffnen, verfälschte Druckköpfe M PNG/JPEG per Signatur, SVG nur per <svg in den ersten 2 kB (schwach); max. 2 MB; Abruf mit nosniff und Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data: src/server/controllers/document-branding.controller.ts; tests/document-branding.repository.test.ts (nur Repository, kein Routentest) teilweise, Routentest fehlt; Rechte siehe E3
R12 Keine Nachvollziehbarkeit bei geteilten Projekten: der Projektverlauf hat actorId, die API setzt ihn aber nie A4 Änderungen keinem Nutzer zuordenbar M angemeldeten Nutzer (req.authUser) bis in projectCommandService durchreichen grep actorId src/server leer; Spalte project_revisions.actor_id offen
R13 Secrets oder echte Kundendaten im Repo A6 Zugangsdaten bzw. Kundendaten öffentlich H .env/.env.* und docs/spec/kbp-Stromkreis.xlsx gitignored, Testdaten synthetisch, gitleaks per pre-commit .gitignore; make secrets belegt
R14 Abhängigkeiten (u. a. exceljs parst Fremdinhalt) A6 Codeausführung, DoS M npm audit in make check, Lockfile, neue Pakete per dependency-vetting make audit: 6 moderate ohne nicht-brechenden Fix (drizzle-kit/esbuild, exceljs/uuid), kein High/Critical offen bis Eintrag in RELEASE-AUSNAHMEN.md oder Update

Design-Entscheidungen

Hier steht nur, was Julian bzw. Christian ausdrücklich entschieden haben. Ein Agent trägt hier nichts als entschieden ein.

D1: Projekte sind teamweit geteilt (kein Besitz pro Projekt).

  • Entschieden von: Julian, am 17.10.2026 gegenüber Christian bestätigt (beantwortet E1). Eingetragen von einem Agenten auf Anweisung von Christian.
  • Begründung: Die Software läuft nur im eigenen Team. Alle angemeldeten Nutzer sollen alle Projekte sehen und bearbeiten.
  • Gilt für: jede Instanz, auf der nur Mitglieder des eigenen Teams Konten haben; heute die Instanz auf cj-ki (leistungsbilanz.jansseneconsult.de, leistungsbilanz.electric-consult.de). Admins legen Konten nur für Teammitglieder an, keine Externen.
  • Neu bewerten: sobald Externe (z. B. Kunden, Subplaner) ein Konto bekommen sollen. Dann wird das System überarbeitet (Besitz bzw. Freigabe pro Projekt), bevor das erste externe Konto angelegt wird; idorGap entfernen, die IDOR-Tests müssen grün werden (authz-idor-audit).
  • Folge: ASVS-light B1/B2 sind für diese Instanz nicht anwendbar (R3). idorGap in tests/security/security.config.ts bleibt gesetzt und macht den Ist-Zustand sichtbar.

Offene Entscheidungen

E1 (geteilte Projekte) ist mit D1 beantwortet.

E2 (H): Bleibt die Instanz ohne Authelia öffentlich erreichbar? Laut Caddy-Kommentar seit 28.09.2026 bewusst so, wer das entschieden hat, steht dort nicht. Wegen D1 heißt ein erratenes Passwort Zugriff auf alle Kundenprojekte. Alternativen: Authelia bzw. VPN vorschalten oder 2. Faktor in der App.

E3 (M): Dürfen alle user Büroprofil und Logo ändern, oder nur admin?

Nächste Schritte (technisch, ohne Entscheidung machbar)

  • R6: Port 3090 nur für Caddy, COOKIE_SECURE=true in der Live-.env prüfen
  • R7: globales JSON-Limit klein, 25 MB nur auf Import-Routen nach dem Auth-Guard, mit Test
  • R8: Zip-Bomben-Test und Grenze für die entpackte Größe vor workbook.xlsx.load
  • R11: Routentest für den Logo-Upload (SVG mit Skript, falscher Typ, Größe, ohne Login)
  • R12: actorId aus der Session in den Projektverlauf schreiben, mit Test