Deploy auf cj-ki: Web-Container nur noch 127.0.0.1:3090, Caddy über das Docker-Netz, COOKIE_SECURE=true geprüft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 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, Host0.0.0.0:3090) → Next-Rewrite → API (Express 5, Port 3000, nur im Compose-Netz) → SQLitedata/leistungsbilanz.db - Browser im LAN →
http://192.168.0.133:3090direkt, ohne TLS und am Caddy vorbei - Anmeldung nur in der App:
POST /api/auth/login→ Session-Cookielb_session(httpOnly, SameSite=Lax,SecureperCOOKIE_SECURE), Sessions im Speicher der API - Schreibzugriffe: zusätzlich Header
X-Leistungsbilanz-Request: 1,Sec-Fetch-Sitewird geprüft - Rollen:
admin(zusätzlich/api/users/*) unduser(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
- Internet/LAN → Web: Identität wird nur in der API über
lb_sessionfestgestellt. Kein Proxy-Header wird geglaubt (gefälschtesX-Remote-Userwird im Baseline-Test abgelehnt). - Web → API: die API ist nicht auf dem Host veröffentlicht (
expose), der Next-Rewrite reicht Cookie und Header durch. - API → Projektdaten: nach dem Login gibt es keine Prüfung pro Projekt (R3).
- 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 |
Seit 2026-10-17: compose.local.yaml bindet 127.0.0.1:3090:3001, Caddy erreicht leistungsbilanz-web-1:3001 über das Docker-Netz nginx-proxy-manager_default; 192.168.0.133:3090 aus dem LAN geprüft zu, beide HTTPS-Hosts 200; COOKIE_SECURE=true in der Live-.env geprüft. Kein automatischer Test (Deploy-Konfiguration liegt außerhalb des Repos) |
behoben (Deploy cj-ki) |
| 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;
idorGapentfernen, die IDOR-Tests müssen grün werden (authz-idor-audit). - Folge: ASVS-light B1/B2 sind für diese Instanz nicht anwendbar (R3).
idorGapintests/security/security.config.tsbleibt 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)
R6erledigt 2026-10-17 (Port nur noch 127.0.0.1, Caddy über Docker-Netz,COOKIE_SECURE=true)- 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:
actorIdaus der Session in den Projektverlauf schreiben, mit Test