Fachdokumente nach docs/, Entscheidungen festgehalten

CLAUDE.md und README.md verweisen auf docs/, die Dateien lagen aber im
Wurzelverzeichnis. Verschoben.

docs/13-entscheidungen.md hält fest, wo die Dokumente sich widersprechen und
wie es aufgelöst wurde. Der wichtigste Punkt: 04-datenmodell.md verlangt eine
Regel, die jedes UPDATE auf CaseEntryVersion verwirft, 05-akten-und-vorlagen.md
verlangt, dass eine Umklassifizierung rückwirkend auf alle Versionen wirkt.
Beides zusammen geht nicht. Aufgelöst über einen Trigger, der die Tabelle
anhängend hält, aber das Anheben von minClearance zulässt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harlekin
2026-09-01 00:00:58 +02:00
co-authored by Claude Opus 5
commit 4446e3f0ac
16 changed files with 1919 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# Dateien und Sicherheit
## Bedrohungsmodell, ehrlich
Geschlossene Gruppe, alle kennen sich. Das realistische Risiko ist nicht die geschmuggelte
Ransomware, sondern eine hochgeladene SVG- oder HTML-Datei, die im Browser eines Kollegen
Skriptcode ausführt und dessen Session übernimmt. Deshalb die Maßnahmen nach Wirkung sortiert
und nicht nach Auffälligkeit.
## Prüfkette
```
Upload → quarantine/<uuid> → Typprüfung → ClamAV → Neu kodieren → store/<uuid>
│ │
└─────────────┴──► ScanStatus = BLOCKED
```
### 1. Annahme
- Ziel ist `quarantine/`, Dateiname wird durch die Attachment-UUID ersetzt
- Originalname nur in der Datenbank, beim Ausliefern wieder gesetzt
- Limits: 25 MB pro Datei, 500 MB Kontingent pro Nutzer, 20 Uploads pro Minute
- Der Datensatz wird sofort mit `scanStatus = PENDING` angelegt, damit die UI ihn zeigen kann
### 2. Typprüfung, Whitelist über Magic Bytes
Erlaubt: `image/png`, `image/jpeg`, `image/webp`, `application/pdf`, `text/plain`.
Geprüft wird mit `file-type` anhand des Inhalts, **nie anhand der Endung**. Stimmt der
erkannte Typ nicht mit dem gemeldeten überein, ist die Datei blockiert.
Ausdrücklich verboten, auch wenn es bequem wäre: SVG (führt Skripte aus), HTML, ZIP und
andere Archive, Office-Dokumente mit Makros, alles Ausführbare.
### 3. ClamAV
Eigener Container `clamav/clamav:stable`, angebunden über den clamd-Unix-Socket, geteiltes
Volume mit `quarantine/`. `freshclam` läuft im Container. Timeout 30 Sekunden, bei Timeout
`FAILED` und manuelle Freigabe durch einen Admin möglich.
### 4. Neu kodieren
- Bilder: einmal durch `sharp` (`.rotate().toFormat(...)`). Entfernt EXIF, zerstört angehängte
Nutzlast, erzeugt gleich das Vorschaubild (max. 480 px) und einen Blurhash für das
Platzhalterbild.
- PDF: `qpdf --decrypt --remove-page-labels` und Entfernen eingebetteter Dateien und
JavaScript. Erste Seite wird als Vorschaubild gerendert.
- Text: Zeichensatz nach UTF-8 normalisieren, Größe prüfen.
### 5. Freigabe
SHA-256 berechnen, in `store/` verschieben, `scanStatus = CLEAN`, SSE-Ereignis
`attachment.scanned` an alle offenen Ansichten der Akte.
Gleicher Hash bereits vorhanden: nur einen Verweis anlegen statt erneut zu speichern.
## Auslieferung
Alles über `GET /api/files/[id]`, niemals als statische Datei.
```
Content-Disposition: attachment; filename="protokoll_karlsen.pdf"
X-Content-Type-Options: nosniff
Cache-Control: private, max-age=31536000, immutable
Content-Security-Policy: default-src 'none'; sandbox
```
Der Endpunkt prüft vorher die Sichtbarkeit der zugehörigen Akte und protokolliert den Zugriff
ab Stufe 3. Vorschaubilder laufen über denselben Endpunkt mit `?variant=thumb` und dürfen
inline ausgeliefert werden, weil sie serverseitig neu kodiert wurden.
Wenn möglich: eigene Subdomain `files.afis.example.de` ohne Session-Cookie, mit signierter,
kurzlebiger URL. Dann kann selbst eine durchgerutschte Datei keine Session stehlen.
## Weitere Härtung
- Strikte CSP auf der App: `script-src 'self'`, kein `unsafe-inline` (Next.js Nonce nutzen)
- Rate Limits auf Login, Upload, Suche und Export
- Alle Server Actions prüfen die Berechtigung erneut, egal was das UI vorher versteckt hat
- Kein `dangerouslySetInnerHTML` für Nutzerinhalte. Markdown über `remark` mit
`rehype-sanitize`, feste Allowlist.
- Datenbank-Nutzer der App ohne `SUPERUSER`, Migrationen über einen zweiten Nutzer