Audit 12.09.2026: Rechte, Gäste, Kalender, Push, SSO — 16 von 18 Befunden #2

Merged
thiel merged 10 commits from audit-befunde into zuweisung-meldet 2026-09-12 15:22:14 +00:00
Owner

Setzt die Gegenprüfung des externen Audits um (claude/sote-audit-gegenpruefung.md im Projekt). Basis ist zuweisung-meldet (#1), weil patch() dort die neue Signatur bekam — nach dem Merge von #1 kann die Basis auf main wechseln.

Zehn Commits, einer je Befundgruppe, jeder mit Tests, die ohne den Fix rot waren (jeweils gegen den alten Stand geprüft). pnpm check grün: 237 / 77 / 471.

Sofort (liefen in Produktion falsch)

  • F06 sweepFiles kannte nur storage_key und löschte täglich jede Bildvariante älter als eine Stunde. Jetzt zählt jeder Schlüssel einer Zeile; readFileOf fällt bei fehlender Variante aufs Original zurück.
  • F07 /kalender/:token.ics stand hinter dem statischen Rückfall — Kalenderabos haben nie funktioniert. Block vor den Rückfall, HTTP-Test.
  • F08 Kein Handler für push.send — Push für Zuweisungen und Kommentare kam nie an. Handler in push.ts; Migration 0039 verwirft den Rückstau, damit nach dem Deploy keine Flut alter Meldungen kommt.
  • F09 SSO-Cookie war [object Object]. Ein sessionCookie(token, days) für alle fünf Stellen; Secure bei https://-Basisadresse (F12).

Rechte

  • F01 effectiveListLevel() (Maximum über Rolle und Gruppen, Eigentümer = admin) wird einmal nach memberWorkspace() berechnet; listAccessNeeded(path, method) entscheidet fail-closed: persönliche und verwaltende Wege laufen vorbei, Lesen braucht viewer, Schreiben editor. Gast mit Konto → 403 no_list_level, viewer beim Schreiben → 403 read_only. /api/me liefert listLevel je Arbeitsbereich.
  • F02 GET /api/shares gibt Tokens nur noch an Schreibende; mayManage in der Antwort, der Bildschirm versteckt Anlegen/Widerrufen für Mitleser.
  • F03 Gast-Detail: known nur noch Schlagwörter des freigegebenen Projekts.
  • F13 deliver() prüft Mitgliedschaft; removePerson nimmt Zuweisungen und Gruppenmitgliedschaften im Arbeitsbereich mit.

Gastpfade

  • F05 Gast-PATCH durch readPatch, as never weg.
  • F17 createFromLine bekommt may: { assign: false, pinProject: true } und die Browser-Zone; @name kommt als unknownAssignees zurück, #projekt wird nicht mehr nachträglich korrigiert. (Das Zuweisungszeichen ist @, nicht + wie im Audit — + ist das Schlagwort.)
  • F04 PUT …/files/:id/web mit derselben Grenze wie der Anhang, 413.

SSO und Anmeldung

  • F10 readWhoami verlangt email_verified === true.
  • F11 sote_sso-Keks bindet den Vorgang an den Browser (Hash in sso_flows.browser_hash, Migration 0040); safeNextPath lehnt //… ab.
  • F12 Throttle: 10 Fehlversuche je Konto, 100 je Herkunft, 15 Minuten, 429 mit Retry-After, vor scrypt. SOTE_TRUST_PROXY=1 für X-Forwarded-For (compose + .env.example).

Betrieb und Wächter

  • F14 DELETE /api/push nur eigenes Gerät; Endpunkt muss https: sein und darf nicht nach innen zeigen (keine Dienst-Allowlist, UnifiedPush bleibt möglich); max. 20 Geräte; 10 s Timeout.
  • F16 check-routes-reachable kennt jetzt !path.startsWith()-Zweige und Regex-Wege — gegen den alten Stand fällt er mit /kalender/x … verschluckt. fileURLToPath in zwei Scripts.

Nicht umgesetzt (bewusst)

  • F15 Konflikterkennung (Version/ETag) — Designentscheidung, eigener PR.
  • F18 Export als vollständige Sicherung — eigener PR; kurzfristig reicht ein Hinweis im Export-Bildschirm.
  • Oberfläche für viewer/guest: der Server sagt 403 und /api/me nennt die Stufe, aber die Bildschirme verstecken Bedienelemente noch nicht (nur Freigaben tut es). Ein Gast mit Konto sieht jetzt Fehler statt fremder Daten — richtig, aber noch nicht schön.

Beim Deploy

  1. Migrationen 0039 und 0040 laufen automatisch.
  2. Wer die Gast-Rolle als billiges Nur-Lesen benutzt hat: die Leute sehen ab jetzt nichts mehr. Dafür gibt es viewer.
  3. Bei SSO: der Anbieter muss email_verified: true liefern (Authelia/Keycloak/Authentik tun das für bestätigte Adressen).
  4. Hinter einem Reverse Proxy SOTE_TRUST_PROXY=1 setzen.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA

Setzt die Gegenprüfung des externen Audits um (`claude/sote-audit-gegenpruefung.md` im Projekt). Basis ist `zuweisung-meldet` (#1), weil `patch()` dort die neue Signatur bekam — nach dem Merge von #1 kann die Basis auf `main` wechseln. Zehn Commits, einer je Befundgruppe, jeder mit Tests, die ohne den Fix rot waren (jeweils gegen den alten Stand geprüft). `pnpm check` grün: 237 / 77 / 471. ## Sofort (liefen in Produktion falsch) - **F06** `sweepFiles` kannte nur `storage_key` und löschte täglich jede Bildvariante älter als eine Stunde. Jetzt zählt jeder Schlüssel einer Zeile; `readFileOf` fällt bei fehlender Variante aufs Original zurück. - **F07** `/kalender/:token.ics` stand hinter dem statischen Rückfall — Kalenderabos haben nie funktioniert. Block vor den Rückfall, HTTP-Test. - **F08** Kein Handler für `push.send` — Push für Zuweisungen und Kommentare kam nie an. Handler in `push.ts`; Migration 0039 verwirft den Rückstau, damit nach dem Deploy keine Flut alter Meldungen kommt. - **F09** SSO-Cookie war `[object Object]`. Ein `sessionCookie(token, days)` für alle fünf Stellen; `Secure` bei `https://`-Basisadresse (F12). ## Rechte - **F01** `effectiveListLevel()` (Maximum über Rolle und Gruppen, Eigentümer = admin) wird einmal nach `memberWorkspace()` berechnet; `listAccessNeeded(path, method)` entscheidet fail-closed: persönliche und verwaltende Wege laufen vorbei, Lesen braucht `viewer`, Schreiben `editor`. Gast mit Konto → 403 `no_list_level`, viewer beim Schreiben → 403 `read_only`. `/api/me` liefert `listLevel` je Arbeitsbereich. - **F02** `GET /api/shares` gibt Tokens nur noch an Schreibende; `mayManage` in der Antwort, der Bildschirm versteckt Anlegen/Widerrufen für Mitleser. - **F03** Gast-Detail: `known` nur noch Schlagwörter des freigegebenen Projekts. - **F13** `deliver()` prüft Mitgliedschaft; `removePerson` nimmt Zuweisungen und Gruppenmitgliedschaften im Arbeitsbereich mit. ## Gastpfade - **F05** Gast-PATCH durch `readPatch`, `as never` weg. - **F17** `createFromLine` bekommt `may: { assign: false, pinProject: true }` und die Browser-Zone; `@name` kommt als `unknownAssignees` zurück, `#projekt` wird nicht mehr nachträglich korrigiert. (Das Zuweisungszeichen ist `@`, nicht `+` wie im Audit — `+` ist das Schlagwort.) - **F04** `PUT …/files/:id/web` mit derselben Grenze wie der Anhang, 413. ## SSO und Anmeldung - **F10** `readWhoami` verlangt `email_verified === true`. - **F11** `sote_sso`-Keks bindet den Vorgang an den Browser (Hash in `sso_flows.browser_hash`, Migration 0040); `safeNextPath` lehnt `//…` ab. - **F12** `Throttle`: 10 Fehlversuche je Konto, 100 je Herkunft, 15 Minuten, 429 mit `Retry-After`, vor scrypt. `SOTE_TRUST_PROXY=1` für `X-Forwarded-For` (compose + .env.example). ## Betrieb und Wächter - **F14** `DELETE /api/push` nur eigenes Gerät; Endpunkt muss `https:` sein und darf nicht nach innen zeigen (keine Dienst-Allowlist, UnifiedPush bleibt möglich); max. 20 Geräte; 10 s Timeout. - **F16** `check-routes-reachable` kennt jetzt `!path.startsWith()`-Zweige und Regex-Wege — gegen den alten Stand fällt er mit `/kalender/x … verschluckt`. `fileURLToPath` in zwei Scripts. ## Nicht umgesetzt (bewusst) - **F15** Konflikterkennung (Version/ETag) — Designentscheidung, eigener PR. - **F18** Export als vollständige Sicherung — eigener PR; kurzfristig reicht ein Hinweis im Export-Bildschirm. - **Oberfläche für viewer/guest**: der Server sagt 403 und `/api/me` nennt die Stufe, aber die Bildschirme verstecken Bedienelemente noch nicht (nur Freigaben tut es). Ein Gast mit Konto sieht jetzt Fehler statt fremder Daten — richtig, aber noch nicht schön. ## Beim Deploy 1. Migrationen 0039 und 0040 laufen automatisch. 2. Wer die Gast-Rolle als billiges Nur-Lesen benutzt hat: die Leute sehen ab jetzt nichts mehr. Dafür gibt es `viewer`. 3. Bei SSO: der Anbieter muss `email_verified: true` liefern (Authelia/Keycloak/Authentik tun das für bestätigte Adressen). 4. Hinter einem Reverse Proxy `SOTE_TRUST_PROXY=1` setzen. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F06. `sweepFiles` baute die Menge der bekannten
Schluessel aus `storage_key` allein; die kleine Fassung (`web_key`)
liegt im selben Baum und galt darum nach einer Stunde als Waise. Einmal
am Tag verschwanden gueltige Bildvarianten, ihre Zeile zeigte weiter
auf sie, und `readFileOf` warf danach.

Jetzt zaehlt jeder Schluessel, den eine Zeile nennt. Und wer aus der
Zeit davor eine Zeile ohne Datei geerbt hat, bekommt das Original
statt eines Fehlers: eine Variante ist abgeleitet und kann neu
entstehen.

Zwei Tests, beide ohne den Fix rot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F07. `/kalender/:token.ics` heisst nicht `/api/…`,
und der statische Rueckfall behandelt alles ausserhalb `/api/` als
Datei der Oberflaeche -- also bekam jedes Kalenderprogramm 404, bevor
eine Abfrage lief. Kein Test sah es: `calendar.db.test.ts` rief
`icsByToken` direkt.

Der Block steht jetzt vor dem Rueckfall, und ein Test geht den Weg
ueber HTTP: GET, HEAD, fremder Token (404 aus der Route, nicht aus dem
Dateiausgeber), POST (405). Ohne den Fix rot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F08. `deliver()` legt seit dem ersten Tag Auftraege
der Art `push.send`; kein Modul hat `handle('push.send', …)` gerufen.
Fuenfmal „kein Bearbeiter", dann liegen gelassen. Von aussen unsichtbar,
weil die Aufgabenerinnerungen `pushTo` direkt rufen und darum ankamen --
und in der Notiz zur Zuweisung stand bereits: „ob Push wirklich
rausgeht, ist ungetestet. Der Job liegt."

Jetzt: Bearbeiter in `push.ts`, Nutzlast geprueft statt geglaubt, ein
Konto ohne Geraete ist erledigt und kein Fehler. Migration 0039
markiert den Rueckstau als verworfen, damit nach dem Deploy keine Flut
alter Meldungen auf die Geraete geht. Zwei Tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F09 und F12. Fuenfmal dieselbe Zeile in routes.ts,
und die SSO-Fassung interpolierte `${session}` statt `${session.token}`:
der Keks hiess `[object Object]`, die Anmeldung beim Anbieter gelang,
und in SOTE war man trotzdem niemand. Der Typpruefer sieht das nicht --
in einem Template-Literal ist jedes Objekt ein String.

`sessionCookie(token, days)` nimmt einen String und kann den Fehler
nicht mehr machen. Dazu `Secure`, wenn `SOTE_BASE_URL` mit https://
beginnt -- dieselbe Angabe, aus der die Mail-Links gebaut werden. Das
Abmelden loescht mit denselben Attributen, sonst loescht es nichts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F01, der Kernbefund. `list_level = NULL heisst
wirklich nichts` stand in drei Kommentaren (Konzept §7, bootstrap.ts,
rights.ts) und in keiner Route: `mayWriteLists` wurde nur beim Anlegen
einer Freigabe gefragt, jede Aufgabenroute pruefte die Zeile in
`workspace_members` und sonst nichts. Ein viewer konnte patchen, ein
Gast mit Konto alles lesen und exportieren -- der Schalter, der nichts
bewacht (ADR-0087).

`effectiveListLevel(q, userId, workspaceId)` in settings.ts: das
Maximum ueber Rolle und Gruppenrollen, Eigentuemer = admin, sonst null.
`mayWriteLists` ist jetzt eine Lesart davon.

routes.ts fragt EINMAL nach `memberWorkspace` und entscheidet ueber
`listAccessNeeded(path, method)`: persoenliche und verwaltende Wege
laufen vorbei (die verwaltenden haben eigene Rechte-Waechter, und
Rechte sind die andere Haelfte der Rolle), alles Lesende braucht
viewer, alles Schreibende editor -- als Vorgabe, nicht als Liste,
damit ein neuer Weg geschuetzt zur Welt kommt. Erinnerung, Kalender
und eigene Listenansicht sind persoenlich und bleiben dem viewer.

`/api/me` nennt die Stufe je Arbeitsbereich, damit die Oberflaeche
keine Knoepfe zeigt, die 403 antworten (SONE ADR-0095). Das Verstecken
selbst ist noch offen.

Drei Tests ueber HTTP: viewer liest und jede schreibende Route sagt
403 ohne etwas zu aendern; Gast sieht keine Liste, behaelt sein Konto;
eine Editor-Gruppe hebt den Gast hinauf. Zwei davon ohne den Fix rot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F02 und F03.

F02: `GET /api/shares` gab jedem Mitglied die entsiegelten Tokens. Ein
viewer las einen Bearbeitungslink ab und schrieb damit anonym -- mehr,
als seine Rolle ihm gab. Der Token kommt jetzt nur, wer `mayWriteLists`
hat (dieselbe Frage wie bei POST und DELETE); die Zeile bleibt, damit
jeder sieht, dass es eine Freigabe gibt. `mayManage` steht in der
Antwort, und der Bildschirm zeigt einem Mitleser weder Anlegen noch
Widerrufen -- statt Knoepfen, die 403 sagen (SONE ADR-0095).

F03: `known` in der Gast-Detailansicht kam aus `labels WHERE
workspace_id` -- jedes Schlagwort jedes Projekts, auch der vertraulichen.
Der Gast sieht EIN Projekt (Konzept §7); sein Schlagwortfeld schlaegt
jetzt vor, was in diesem Projekt vergeben ist, und nichts darueber.

Zwei Tests ueber HTTP, beide ohne die Fixes rot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F13. Die Empfaenger einer Meldung kommen aus Urheber
und Zustaendigen einer Aufgabe, und beides ueberlebte den Austritt:
`removePerson` loeschte nur die Mitgliedschaft, `deliver` fragte nicht
nach ihr. Ein entferntes Konto bekam weiter Mails mit Aufgabentitel und
Kommentartext -- am empfindlichsten beim Ausscheiden Externer.

`deliver()` prueft die Mitgliedschaft dort, wo die Meldung entsteht,
fuer alle fuenf Aufrufer. `removePerson` nimmt in einer Transaktion
auch Zustaendigkeiten und Gruppenmitgliedschaften im Arbeitsbereich
mit -- Kommentare und Urheberschaft bleiben, Geschichte wird nicht
umgeschrieben.

Das Fixture in deliver.db.test.ts schrieb keine Mitgliedschaften und
prüfte damit eine Form, die Produktion nie schreibt (SONE ADR-0102);
jetzt schreibt es sie. Zwei neue Tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F05, F17, F04.

F05: der Gast-PATCH reichte `planned`/`due` roh an `patch()` weiter,
mit `as never` am Typpruefer vorbei -- und `patch` kennt `plannedAt`/
`dueAt`. Ein Termin allein hiess „nichts zu aendern", mit Titel ging
die Anfrage durch und der Termin blieb. Jetzt liest `readPatch` aus
routes.ts beide Pfade, und der Cast ist weg.

F17: die Schnellerfassung loeste `@name` auch fuer Gaeste auf, trug zu
und meldete der Person -- die direkte Bearbeitung verbot genau das.
`#projekt` wurde erst NACH dem Anlegen mit einem zweiten UPDATE
zurueckgesetzt. Und die Zone des Browsers fehlte: „morgen 9 Uhr" eines
Gasts in Berlin war 9 Uhr UTC. `createFromLine` bekommt jetzt
Faehigkeiten (`may: { assign, pinProject }`) und die Zone; der Name
kommt als `unknownAssignees` zurueck statt still zu verschwinden.

F04: `PUT …/files/:id/web` sammelte den ganzen Koerper, bevor es
irgendetwas pruefte. Jetzt dieselbe Grenze wie beim Anhang und bei der
Mitgliederroute, 413 beim Ueberschreiten.

Drei Tests ueber HTTP, alle ohne die Fixes rot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Audit 12.09.2026, F10, F11, F12.

F10: `whoami` uebernahm `email` ohne `email_verified`. Die Adresse ist
der Schluessel, mit dem das erste Anmelden ein bestehendes Konto findet
und das Subjekt daran bindet -- ein Anbieter mit selbst gewaehlten,
unbestaetigten Adressen liesse damit jeden ein fremdes Konto
beanspruchen. `readWhoami` verlangt `email_verified === true`; fehlt
der Claim, ist das keine Bestaetigung.

F11: nichts band den Vorgang an den Browser, der ihn begonnen hat
(Login-CSRF). Jetzt ein Geheimnis im `sote_sso`-Keks (zehn Minuten,
nur `/api/sso/`), sein Hash am Vorgang (Migration 0040), und `takeFlow`
verlangt beides -- und verbrennt den Vorgang, wenn der Keks fehlt.
Dazu `safeNextPath`: die alte Regel hielt `//boese.example` am Punkt
fern, nicht am Prinzip; `//attacker` kam durch. Der Test prueft jetzt
die Funktion statt eine Kopie der Regel.

F12: `/api/session` rief scrypt fuer jeden Versuch. `Throttle` zaehlt
Fehlversuche je Konto (10) und je Herkunft (100) in fuenfzehn Minuten,
antwortet 429 mit Retry-After, VOR dem Hash. `X-Forwarded-For` nur mit
`SOTE_TRUST_PROXY=1`, und dann der letzte Eintrag.

Sieben neue Tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
Push: das eigene Geraet abmelden, kein Ziel nach innen -- und Waechter, die sehen
All checks were successful
Build image / check (push) Successful in 59s
Build image / image (push) Successful in 1m12s
071740808b
Audit 12.09.2026, F14 und F16.

F14: `DELETE /api/push` loeschte ueber den Endpunkt allein -- wer den
eines anderen kannte, meldete dessen Geraet ab. Jetzt `(user_id,
endpoint)`. Ein Endpunkt muss `https:` sein und darf nicht nach innen
zeigen (localhost, private Bereiche, .local); KEINE Liste zugelassener
Dienste, damit Firefox-Autopush und UnifiedPush weiter gehen. Hoechstens
20 Geraete je Konto; der Versand hat zehn Sekunden.

F16: `check-routes-reachable` kannte nur `/^\/api…/`-Muster und
`path === '…'`-Literale -- den Kalender hinter `!path.startsWith('/api/')`
konnte er nicht sehen und meldete „alle erreichbar". Jetzt ist der
startsWith-Zweig ein Muster, jeder Regex ein Weg (geprueft an seinem
woertlichen Anfang), und gegen den alten Stand faellt der Waechter mit
„/kalender/x … verschluckt von alles ausser /api/…". `check-env-passed`
und `check-styles` nehmen `fileURLToPath` statt `.pathname`.

`SOTE_TRUST_PROXY` steht in compose und .env.example -- der
env-Waechter hat es verlangt, zu Recht.

`pnpm check` gruen: 237 / 77 / 471.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kdn7zzrYhhwUwH8PzMteCA
thiel merged commit 071740808b into zuweisung-meldet 2026-09-12 15:22:14 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
thiel/sote!2
No description provided.