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>
83 lines
3.5 KiB
Markdown
83 lines
3.5 KiB
Markdown
# 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
|