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>
3.5 KiB
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 = PENDINGangelegt, 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-labelsund 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', keinunsafe-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
dangerouslySetInnerHTMLfür Nutzerinhalte. Markdown überremarkmitrehype-sanitize, feste Allowlist. - Datenbank-Nutzer der App ohne
SUPERUSER, Migrationen über einen zweiten Nutzer