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>
This commit is contained in:
parent
1cf406d216
commit
e51752f3e1
3 changed files with 30 additions and 40 deletions
19
AGENTS.md
19
AGENTS.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue