- 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>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
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);
|
||
}
|
||
}
|