docs(security): Design-Entscheidung D1 (Projekte teamweit geteilt) eintragen
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

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>
This commit is contained in:
Grovy311 2026-10-17 12:56:23 +02:00
parent 1cf406d216
commit e51752f3e1
3 changed files with 30 additions and 40 deletions

View file

@ -798,16 +798,15 @@ Rules for agents working on this repo:
Open security points (state at adoption, 2026-10-15; updated 2026-10-17): Open security points (state at adoption, 2026-10-15; updated 2026-10-17):
- **Threat model: `docs/THREATMODEL.md`** (German, since 2026-10-17). Keep it current when - **Threat model: `docs/THREATMODEL.md`** (German, since 2026-10-17). Keep it current when
auth, roles, routes, imports/exports or deployment change. Its "Design-Entscheidungen" auth, roles, routes, imports/exports or deployment change. Only Julian or Christian record
section is still empty: only Julian or Christian record decisions there, agents never do. decisions in its "Design-Entscheidungen" section; agents only write down what they state.
- **Shared-projects decision pending (THREATMODEL R3 / E1, blocks release).** B1/B2 (object - **Projects are shared team-wide by design (THREATMODEL D1, Julian, 2026-10-17).** Every
access, list filtering) are K items in the ASVS-light checklist. `pre-release-gate` reports signed-in user can read and change every project, because the software runs only within
No-Go until E1 is answered and recorded as a design decision (who decided, when, why, which the own team. ASVS-light B1/B2 are therefore not applicable (R3). The IDOR tests in
instance/team, when to re-evaluate). `idorGap` alone is not evidence. `baseline.test.ts` run but are reported as TODO via `idorGap` in `security.config.ts`.
- **Projects have no owner.** Every signed-in user can read and change every project. - **Before any external account (customers, subcontractors) is created:** introduce
The IDOR tests in `baseline.test.ts` run but are reported as TODO via `idorGap` in per-project ownership/sharing, remove `idorGap`, the tests must turn green. D1 no longer
`security.config.ts`. If E1 is answered "no": introduce per-project ownership/sharing, holds once externals get accounts.
then remove `idorGap`; the tests must turn green.
- Further open items from the threat model (no decision needed, see its "Nächste Schritte"): - Further open items from the threat model (no decision needed, see its "Nächste Schritte"):
R6 live port 3090 reachable over plain HTTP in the LAN, R7 global 25 MB JSON limit before R6 live port 3090 reachable over plain HTTP in the LAN, R7 global 25 MB JSON limit before
the auth guard, R8 no XLSX zip-bomb guard before `workbook.xlsx.load`, R11 no route test the auth guard, R8 no XLSX zip-bomb guard before `workbook.xlsx.load`, R11 no route test

View file

@ -63,7 +63,7 @@ Gesundheitsdaten: keine. Personenbezug: nur Benutzerkonten.
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| 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 | | 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 | | 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. Gedacht als teamweit geteilte Projekte, aber **noch nicht als Entscheidung festgehalten** (E1) | IDOR-/Listen-Tests in `baseline.test.ts` laufen, zählen per `idorGap` (`tests/security/security.config.ts`) als TODO. `idorGap` allein ist kein Beleg | **offen, Entscheidung E1 ausstehend** | | 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 | | 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 | | 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** | | 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** |
@ -81,34 +81,26 @@ Gesundheitsdaten: keine. Personenbezug: nur Benutzerkonten.
Hier steht nur, was Julian bzw. Christian ausdrücklich entschieden haben. Ein Agent trägt hier Hier steht nur, was Julian bzw. Christian ausdrücklich entschieden haben. Ein Agent trägt hier
nichts als entschieden ein. nichts als entschieden ein.
*(noch keine)* **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 ## Offene Entscheidungen
**E1 (K, blockiert den Release): Sind Projekte bewusst für alle angemeldeten Nutzer geteilt?** E1 (geteilte Projekte) ist mit D1 beantwortet.
Heute sieht und ändert jeder angemeldete Nutzer jedes Projekt (R3). Damit B1/B2 im
`pre-release-gate` als „nicht anwendbar“ gelten, braucht es unter „Design-Entscheidungen“ einen
Eintrag mit:
- **Wer** hat entschieden (Julian? Christian? beide?) und **wann**
- **Warum** (z. B. alle Nutzer arbeiten im selben Planungsteam an denselben Projekten,
Vertretung ohne Freigabeschritt)
- **Für welche Instanz und welches Team**: gilt das für `leistungsbilanz.jansseneconsult.de` und
`leistungsbilanz.electric-consult.de` (dieselbe Instanz auf cj-ki)? Wer bekommt dort Konten,
nur das eigene Team oder auch Externe (Kunden, Subplaner)?
- **Bis wann / unter welcher Bedingung** das neu bewertet wird (z. B. sobald externe Konten
dazukommen oder eine zweite Firma die Instanz nutzt)
Vorschlag für den Eintrag, **erst nach Bestätigung durch Julian** übernehmen:
> D1: Projekte sind teamweit geteilt (kein Besitz pro Projekt). Entschieden von <Name> am
> <Datum>. Begründung: <…>. Gilt für die Instanz <…> mit Konten nur für <Team>. Neu bewerten,
> sobald <Bedingung>. Folge: ASVS-light B1/B2 nicht anwendbar, `idorGap` in
> `tests/security/security.config.ts` bleibt bis dahin gesetzt.
Lautet die Antwort „nein“: Besitz bzw. Freigabe pro Projekt bauen, `idorGap` entfernen, die
IDOR-Tests müssen grün werden (`authz-idor-audit`).
**E2 (H): Bleibt die Instanz ohne Authelia öffentlich erreichbar?** Laut Caddy-Kommentar seit **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. Mit R3 heißt ein erratenes 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 Passwort Zugriff auf alle Kundenprojekte. Alternativen: Authelia bzw. VPN vorschalten oder
2. Faktor in der App. 2. Faktor in der App.

View file

@ -94,12 +94,11 @@ const config = {
return (await res.json()) as Record<string, unknown>; return (await res.json()) as Record<string, unknown>;
}, },
// OFFENE LÜCKE: Projekte haben keinen Besitzer, jeder angemeldete Nutzer sieht und // BEWUSSTE LÜCKE (Design-Entscheidung D1 in docs/THREATMODEL.md, Julian, 17.10.2026):
// bearbeitet alle Projekte. Gedacht als teamweit geteilt, die Entscheidung (wer, warum, // Projekte haben keinen Besitzer, jeder angemeldete Nutzer sieht und bearbeitet alle
// welche Instanz/welches Team) ist aber noch nicht festgehalten: docs/THREATMODEL.md R3 // Projekte, weil die Software nur im eigenen Team läuft. Die IDOR-Tests laufen trotzdem
// und offene Entscheidung E1. Bis dahin gilt B1/B2 im pre-release-gate als offen. // und zeigen den Ist-Zustand, zählen aber als TODO statt als Fehler. Bevor Externe ein
// Die IDOR-Tests laufen trotzdem und zeigen den Ist-Zustand, zählen aber als TODO statt // Konto bekommen: Besitz bzw. Freigabe pro Projekt einführen, diese Zeile entfernen,
// als Fehler. Wird Besitz bzw. Freigabe pro Projekt eingeführt, diese Zeile entfernen,
// dann müssen sie grün werden. // dann müssen sie grün werden.
idorGap: "Projekte sind für alle angemeldeten Nutzer freigegeben (kein Besitz pro Projekt)", idorGap: "Projekte sind für alle angemeldeten Nutzer freigegeben (kein Besitz pro Projekt)",
idorCases: [ idorCases: [