# 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/ → Typprüfung → ClamAV → Neu kodieren → store/ │ │ └─────────────┴──► 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 anhand des Inhalts, **nie anhand der Endung**. Stimmt der erkannte Typ nicht mit dem gemeldeten überein, ist die Datei blockiert. Gebaut mit einer eigenen Signaturtabelle statt mit `file-type`, begründet in `docs/13-entscheidungen.md`. Die Signatur muss an Position 0 stehen; ein Vorspann davor wird nicht akzeptiert. Ausgeliefert wird unter der Endung des **erkannten** Typs, nicht unter der des hochgeladenen Namens. 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. Übergeben wird per `INSTREAM`, nicht als Pfad: dann muss der Scanner die Datei nicht sehen können. `freshclam` läuft im Container. Timeout 30 Sekunden, bei Timeout `FAILED` und manuelle Freigabe durch einen Admin möglich. `BLOCKED` und `FAILED` sind nicht dasselbe: das eine heisst „ist gefährlich", das andere „wissen wir nicht". Nur `FAILED` lässt sich freigeben, und die Freigabe wiederholt die Kette ohne den Scanner — Typprüfung und Neukodierung laufen dabei mit. ### 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