fix(security): Befunde aus dem Security-Audit beheben #7

Merged
bjoernpoettker merged 10 commits from fix/security-audit into main 2026-11-04 10:03:36 +00:00
Owner

Behebt die Befunde aus dem Security-Audit (Lauf 1, Commit c1afa02). Zu jedem Fix gibt es Tests; die 6 Fingerprint-/Kennzahlen-Tests, die auch auf main fehlschlagen (pdf.js unter Jest), sind unverändert.

Was sich ändert

Hoch

  • Rechteausweitung über API-Keys: /api/api-keys nur noch mit MANAGE_SETTINGS. API-Keys umgehen keine Rechteprüfung mehr, sie gelten nur auf Routen mit @AllowApiKey() (Paperless-Webhook, externer Upload, checksum, Agent-Routen des Etikettendrucks).
  • Serverdateien per Mail auslesen: send-email prüft den Body streng, nodemailer läuft mit disableFileAccess/disableUrlAccess.
  • DoS durch PDF-Dekompressionsbomben: Vorab-Check (256 MB entpackt) vor pdf.js (Fingerprint) und pdf-lib (E-Rechnung). Ghostscript läuft mit -dSAFER statt -dNOSAFER, mit Timeout und max. 500 Seiten.

Mittel/Niedrig

  • PermissionsGuard deny by default: jede Route braucht @RequirePermissions, @AllowAnyAuthenticated oder @Public. routen-rechte.spec.ts prüft alle Routen.
  • Posteingangs-Maske ändert nur Dokumente im eigenen Bereich (Tag 1 bzw. Manuell-Tag); Freigabe (CF15) und Zahlung (CF16) nur mit der jeweiligen Rolle.
  • Etiketten-Agent: printed/error nur für abgeholte, offene Jobs (sonst 409); Feldwerte nur aus der Vorlage und URL-kodiert.
  • Path-Traversal in den Mail-Vorschau-Routen, Symlinks auf der Scan-Freigabe, Exportdateinamen, Belegnummer-URL.
  • JWT: Audience wird gegen OIDC_CLIENT_ID geprüft.

Fachliche Änderungen

  • PM_Buchhaltung umfasst jetzt PM_Posteingang (Menüpunkt und Tagesbericht-Kachel „Posteingang“ für die Buchhaltung).
  • Routen umbenannt: paperless/inbox/:id[/pdf|/preview|/erechnung] → paperless/documents/:id[/pdf|/preview|/erechnung] (ohne Alias).
  • Dokumentsuche und documents/:id liefern nur noch id, Titel, ASN, Datum (kein OCR-Volltext).

Nach dem Deployment prüfen

  • Login funktioniert (Audience-Check). Sonst steht im Backend-Log jwt audience invalid → OIDC_CLIENT_ID prüfen. docker-compose.yml übergibt jetzt standardmäßig dieselbe Client-ID wie das Frontend.
  • Etiketten-Agent holt Jobs ab und meldet printed.
  • Paperless-Webhook und externer Upload laufen (API-Key).
  • Andere Skripte/n8n mit API-Key? Die bekommen jetzt 403 – im Log steht Route und Key-Name.
  • Scanner: Neue Scans landen im Posteingang (Ghostscript mit -dSAFER); Symlinks auf der Freigabe werden übersprungen.
  • Posteingang/Manuell: Vorschau, PDF, Speichern, Verknüpfungssuche; Duplikatvergleich in Scanner und Mail-Import.

🤖 Generated with Claude Code

Behebt die Befunde aus dem Security-Audit (Lauf 1, Commit `c1afa02`). Zu jedem Fix gibt es Tests; die 6 Fingerprint-/Kennzahlen-Tests, die auch auf `main` fehlschlagen (pdf.js unter Jest), sind unverändert. ## Was sich ändert **Hoch** - **Rechteausweitung über API-Keys**: `/api/api-keys` nur noch mit `MANAGE_SETTINGS`. API-Keys umgehen keine Rechteprüfung mehr, sie gelten nur auf Routen mit `@AllowApiKey()` (Paperless-Webhook, externer Upload, `checksum`, Agent-Routen des Etikettendrucks). - **Serverdateien per Mail auslesen**: `send-email` prüft den Body streng, nodemailer läuft mit `disableFileAccess`/`disableUrlAccess`. - **DoS durch PDF-Dekompressionsbomben**: Vorab-Check (256 MB entpackt) vor pdf.js (Fingerprint) und pdf-lib (E-Rechnung). Ghostscript läuft mit `-dSAFER` statt `-dNOSAFER`, mit Timeout und max. 500 Seiten. **Mittel/Niedrig** - `PermissionsGuard` **deny by default**: jede Route braucht `@RequirePermissions`, `@AllowAnyAuthenticated` oder `@Public`. `routen-rechte.spec.ts` prüft alle Routen. - Posteingangs-Maske ändert nur Dokumente im eigenen Bereich (Tag 1 bzw. Manuell-Tag); Freigabe (CF15) und Zahlung (CF16) nur mit der jeweiligen Rolle. - Etiketten-Agent: `printed`/`error` nur für abgeholte, offene Jobs (sonst 409); Feldwerte nur aus der Vorlage und URL-kodiert. - Path-Traversal in den Mail-Vorschau-Routen, Symlinks auf der Scan-Freigabe, Exportdateinamen, Belegnummer-URL. - JWT: Audience wird gegen `OIDC_CLIENT_ID` geprüft. **Fachliche Änderungen** - `PM_Buchhaltung` umfasst jetzt `PM_Posteingang` (Menüpunkt und Tagesbericht-Kachel „Posteingang“ für die Buchhaltung). - Routen umbenannt: `paperless/inbox/:id[/pdf|/preview|/erechnung]` → `paperless/documents/:id[/pdf|/preview|/erechnung]` (ohne Alias). - Dokumentsuche und `documents/:id` liefern nur noch id, Titel, ASN, Datum (kein OCR-Volltext). ## Nach dem Deployment prüfen - [ ] **Login** funktioniert (Audience-Check). Sonst steht im Backend-Log `jwt audience invalid` → `OIDC_CLIENT_ID` prüfen. `docker-compose.yml` übergibt jetzt standardmäßig dieselbe Client-ID wie das Frontend. - [ ] **Etiketten-Agent** holt Jobs ab und meldet `printed`. - [ ] **Paperless-Webhook** und **externer Upload** laufen (API-Key). - [ ] Andere Skripte/n8n mit API-Key? Die bekommen jetzt 403 – im Log steht Route und Key-Name. - [ ] **Scanner**: Neue Scans landen im Posteingang (Ghostscript mit `-dSAFER`); Symlinks auf der Freigabe werden übersprungen. - [ ] **Posteingang/Manuell**: Vorschau, PDF, Speichern, Verknüpfungssuche; Duplikatvergleich in Scanner und Mail-Import. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bjoernpoettker added 10 commits 2026-11-04 09:48:33 +00:00
- ApiKeysController verlangt MANAGE_SETTINGS. Bisher konnte jeder
  OIDC-Benutzer einen API-Key erzeugen, der im PermissionsGuard jede
  Rechteprüfung umgeht (Rechteausweitung zum Admin).
- MailService setzt disableFileAccess/disableUrlAccess für jede Mail.
- send-email im Posteingang prüft den Body streng auf Strings/Zahlen.
  Bisher liest nodemailer bei html: { path: '/proc/self/environ' }
  beliebige Serverdateien ein und verschickt sie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ghostscript läuft auf PDFs aus Mails und vom Scanner jetzt mit -dSAFER
  statt -dNOSAFER (kein Datei-/Prozesszugriff aus PostScript), mit
  Timeout (60 s je Seite, 10 min je Dokument, SIGKILL) und höchstens
  500 gerenderten Seiten. Temp-Dateien werden auch im Fehlerfall
  entfernt. Ungenutztes getPageCount (brauchte Dateizugriff) entfernt.
- Neuer Vorab-Check ueberschreitetDekompressionsLimit (256 MB, streamend,
  erkennt auch Deflate-Ketten und abgeschnittene Streams). Fingerprint
  (pdf.js) und E-Rechnungs-Extraktion (pdf-lib) laden Bomben nicht mehr;
  eine einzelne Mail konnte das Backend sonst per OOM dauerhaft lahmlegen.
- Verschlüsselte PDFs bekommen keinen Inhalts-Fingerprint mehr ('' =
  nicht vergleichbar), weil pdf.js sie erst intern entschlüsselt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PermissionsGuard lehnt Routen ohne @RequirePermissions/@AllowAnyAuthenticated
  ab. API-Keys umgehen keine Rechteprüfung mehr, sondern gelten nur auf
  Routen mit @AllowApiKey (Paperless-Webhook, externer Upload, checksum,
  Agent-Routen des Etikettendrucks).
- Rechte für 24 bisher offene Routen ergänzt, u.a. paperless/documents
  (nur Admin), paperless/documents/:id und Kontonummern (Posteingang/
  manuell), webhook/status (Admin). routen-rechte.spec.ts prüft alle Routen.
- Etiketten-Agent: printed/error nur für abgeholte, offene Jobs (bedingtes
  Update, sonst 409) – ein gedruckter Job kann seine Nummer nicht mehr
  freigeben. Feldwerte nur aus den Eingabefeldern der Vorlage ({number}
  nicht vorgebbar) und URL-kodiert in GET-/Callback-URLs.
- PUT paperless/inbox/:id ignoriert Freigabe (CF15) und Zahlung (CF16)
  ohne die jeweilige Rolle (Funktionstrennung).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PUT paperless/inbox/:id prüft jetzt pro Dokument: Posteingang-Rolle nur
für Dokumente mit Tag 1 (Posteingang), Buchhaltung nur für Dokumente mit
MANUELL_BEARBEITEN_TAG, Admins immer. Bisher ließen sich Mandant (Owner),
Felder und Titel beliebiger Paperless-Dokumente ändern, weil das Backend
mit dem Service-Token arbeitet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Posteingang ist eine abgespeckte Buchhaltung. Bisher bekam
PM_Buchhaltung nur PROCESS_MANUALLY; die Seite „Manuell bearbeiten“
nutzt für Vorschau, PDF, E-Rechnung und Speichern aber Routen mit
VIEW_INBOX – reine Buchhaltungsnutzer konnten dort nichts speichern.

PM_Buchhaltung bekommt jetzt zusätzlich VIEW_INBOX (Backend und Frontend).
permissions.enum.spec.ts vergleicht künftig nicht nur die Gruppennamen,
sondern auch die Berechtigungen pro Gruppe zwischen beiden Kopien.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
„inbox“ war doppelt belegt (/api/inbox = Scanner, /api/paperless/inbox =
Paperless-Posteingang) und passte nicht zur Manuell-Seite, die dieselben
Routen nutzt. Umbenannt (ohne Alias, nur das eigene Frontend nutzt sie):

  PUT  paperless/inbox/:id            → PUT  paperless/documents/:id
  GET  paperless/inbox/pdf/:id        → GET  paperless/documents/:id/pdf
  GET  paperless/inbox/preview/:id    → GET  paperless/documents/:id/preview
  GET  paperless/inbox/:id/erechnung  → GET  paperless/documents/:id/erechnung

Dabei:
- Dokumentsuche (GET documents) wieder für den Posteingang (VIEW_INBOX)
  statt nur Admin – die Verknüpfungssuche im Bearbeiten-Dialog nutzt sie;
  die Einschränkung aus dem vorigen Commit hatte sie gebrochen.
- Suche und GET documents/:id liefern nur noch id, Titel, ASN, Datum
  (kein OCR-Volltext, keine Statusfelder).
- documents/:id/pdf auch für VIEW_SCANNER (Duplikatvergleich im Scanner).
- E-Rechnungs-Vorschlag prüft wie das Speichern pro Dokument (Tag 1 bzw.
  Manuell-Tag); Vorschau/PDF bewusst nicht (Suchergebnisse, Duplikate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attachments/:attachmentId/pages/:page/{thumb,preview} übernahm die ID
ungeprüft in den Dateipfad – mit `..%2Finbox%2F<uuid>` ließen sich
Scanner-Vorschauen anderer Benutzer aus /mnt/data/inbox lesen.

- Routen wandeln attachmentId und page per ParseIntPipe in Zahlen um.
- EmailPageCacheService nimmt nur positive ganze Zahlen als Anhang-ID und
  Seite an, PageCacheService (Scanner) nur UUIDs als Dokument-ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher prüfte das Backend nur Issuer und Signatur. Ein Access-Token, das
derselbe Authentik-Provider für eine andere Anwendung ausgestellt hat,
wurde ebenfalls akzeptiert.

- JwtStrategy verlangt aud == OIDC_CLIENT_ID (jwt-optionen.ts, getestet
  mit echten RS256-Tokens). Ohne OIDC_CLIENT_ID bleibt die Prüfung aus,
  mit Warnung beim Start.
- docker-compose.yml übergibt OIDC_CLIENT_ID jetzt ans Backend, Standard:
  dieselbe Client-ID wie das Frontend (VITE_OIDC_CLIENT_ID).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Scanner: Dateien von der Scan-Freigabe werden nur übernommen, wenn sie
  reguläre Dateien in echten Verzeichnissen sind. chokidar folgt keinen
  Symlinks mehr; beim Kopieren über Mount-Grenzen (/mnt/scans → /mnt/data)
  wird mit O_NOFOLLOW geöffnet und nur exklusiv geschrieben. Bisher holte
  ein Symlink wie scan.pdf -> /proc/self/environ die Zieldatei in den
  Posteingang.
- Export: Dateinamen (enthalten u.a. Barcode-Werte) werden auf ein
  Pfadsegment reduziert – „../“ verlässt den FTP-Zielordner nicht mehr.
- Belegnummer: {Jahr}/{Nummer} werden URL-kodiert in BELEGNUMMER_*_URL
  eingesetzt (Nummer stammt aus Request bzw. gedrucktem Barcode);
  belegnummer/release prüft die Typen im Body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix: Paperless-PDF auch für den Duplikatvergleich im Mail-Import
Build and Push Multi-Platform Images / build-and-push (push) Successful in 34s
d37e02e157
DuplikatVergleichModal lädt das vorhandene Paperless-Dokument über
documents/:id/pdf – auch im Mail-Import (VIEW_MAIL). Die Route verlangte
nur VIEW_INBOX/VIEW_SCANNER, reine Mail-Nutzer bekamen 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bjoernpoettker merged commit 333507df7a into main 2026-11-04 10:03:36 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bjoernpoettker/paperlessmanager#7