Files
paperlessmanager/paperless-backend/src/email/pdf-fingerprint.util.ts
T
bjoernpoettkerandClaude Opus 5.5 de9cb09bcd fix(security): Ghostscript mit -dSAFER, Schutz vor PDF-Dekompressionsbomben
- 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>
2026-11-04 10:46:43 +01:00

52 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as crypto from 'crypto';
import { PDFParse } from 'pdf-parse';
import {
istVerschluesselt,
ueberschreitetDekompressionsLimit,
} from '../common/pdf-dekompression.util';
/**
* Unterhalb dieser Textlänge gilt ein PDF als "ohne Textebene" (z.B. Scan) –
* ein paar Zeichen reichen nicht, um Dokumente sicher zu unterscheiden.
*/
const MIN_TEXT_LENGTH = 20;
/**
* Inhaltsbasierter Fingerprint eines PDFs: SHA-256 über den extrahierten,
* whitespace-normalisierten Text. Anders als die Datei-Checksumme bleibt er
* gleich, wenn ein Absender dasselbe Dokument neu erzeugt (andere
* Erstellungszeit, Dokument-ID o.ä.).
*
* @returns Fingerprint (64 Hex-Zeichen), `''` bei PDFs ohne verwertbaren Text
* (bereits berechnet, aber nicht vergleichbar) oder `null`, wenn das
* PDF nicht gelesen werden konnte. Verschlüsselte PDFs und solche,
* die entpackt zu groß wären, liefern ebenfalls `''`.
*/
export async function computeContentFingerprint(
pdf: Buffer,
): Promise<string | null> {
// pdf.js entpackt ohne Limit – Dekompressionsbomben gar nicht erst laden.
// Verschlüsselte Streams entschlüsselt pdf.js erst intern; vorab sind sie
// nicht prüfbar. Beides gilt als "nicht vergleichbar" ('').
if (
istVerschluesselt(pdf) ||
(await ueberschreitetDekompressionsLimit(pdf))
) {
return '';
}
// pdf.js übernimmt den Puffer, deshalb eine Kopie übergeben
const parser = new PDFParse({ data: new Uint8Array(pdf) });
try {
const { text } = await parser.getText();
const normalized = text.replace(/\s+/g, ' ').trim();
// Seitenmarker ("-- 1 of 1 --") zählen nicht als Inhalt
const inhalt = normalized.replace(/-- \d+ of \d+ --/g, '').trim();
if (inhalt.length < MIN_TEXT_LENGTH) return '';
return crypto.createHash('sha256').update(normalized).digest('hex');
} catch {
return null;
} finally {
await parser.destroy().catch(() => undefined);
}
}