Ein Service-Desk-Team, das ich unterstützt habe, zeigte Kundenanhänge in einem eingebetteten PDF-Fenster an. Eine präparierte „Rechnung“ enthielt einen Link, dessen sichtbarer Text https://portal.example.com lautete, dessen Aktion aber auf eine file://-URI auf einer vom Angreifer kontrollierten UNC-Freigabe zeigte, also genau die Art Ziel, bei der Windows NTLM-Anmeldedaten anbietet, bevor überhaupt ein Browser geöffnet wird. Das Fenster rief beim Klick brav die Shell auf. Kein Exploit, keine fehlerhafte Datei, kein Engine-Bug: nur ein Viewer, der mit feindlichen Eingaben Standarddinge tat. Ein Vorschaufenster in einer Fachanwendung ist eine Ausführungsentscheidung, und PDFium Component, ein PDF-Viewer mit Quellcode für Delphi, C++Builder und Lazarus, legt die Policy-Hooks für diese Entscheidung in Ihre Hand: Schalter beim Laden, ein Ereignis zum Abfangen von Links, Aufrufe für Anhangszugriff und Berechtigungsabfragen. Dieser Artikel geht die Angriffsfläche in der Reihenfolge durch, in der ein Dokument sie erreicht.
Das Bedrohungsmodell eines Vorschaufensters
Seien Sie ehrlich, was „sichere Vorschau“ bedeutet. Der Renderer selbst parst nicht vertrauenswürdige Bytes, und die Härtung der Engine ist der Boden, auf dem Sie stehen. Alles darüber ist Anwendungspolitik: ob Skripte initialisieren, was passiert, wenn ein Nutzer einen Link anklickt, ob eingebettete Dateien auf die Platte gelangen können, ob Zwischenablage und Drucker Türen oder Wände sind. Ein Hinweis zur Eingrenzung vorab: Der Schalter FPDF_SetSandBoxPolicy der Engine hat praktisch nur minimale Wirkung, weil die meisten Engine-Einschränkungen eingebaut sind. Rechnen Sie also keinen Teil Ihrer Isolationsgeschichte darauf an. Für wirklich feindliche Eingabeströme, etwa ein öffentliches Uploadportal, bedeutet echte Isolation, in einem separaten Prozess mit niedrigen Rechten zu rendern. In-Process-Flags sind Policy, kein Containment.
Zwei Flächen werden leicht vergessen, weil nie ein Klick sie berührt. Temporäre Dateien: Wenn Ihre Pipeline eingehende Dokumente vor der Vorschau auf die Platte legt, überleben diese Staging-Kopien die Sitzung, sofern nichts sie nachweislich löscht, und „aus dem Temp-Verzeichnis wiederherstellbar“ hebelt jede Kontrolle aus, die das Fenster selbst durchsetzt. Laden Sie bevorzugt aus dem Speicher über TPdfStreamAdapter, damit die feindlichen Bytes nie einen eigenen Pfad bekommen. Und die Zwischenablage: Eine Vorschau, die Auswählen und Kopieren erlaubt, hat das Dokument bereits exportiert, Bildschirm für Bildschirm.
JavaScript beim Laden abwürgen, nicht in der UI
Dokument-JavaScript in PDFium Component initialisiert nur zusammen mit der Formularumgebung. Laden mit FormFill := False deaktiviert Skripting daher an der Wurzel, statt nur seine Symptome zu unterdrücken:
procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
Pdf.FileName := FilePath;
Pdf.FormFill := False; // no form environment, hence no JavaScript engine
Pdf.Active := True;
FPermissions := Pdf.Permissions; // raw flag word; all bits set = unrestricted
end;
Der Trade-off ist real und gehört in Ihre Spezifikation: Bei deaktivierter Formularausfüllung sind auch legitime AcroForm-Interaktion und Validierungsskripte weg. Felder werden mit ihrer zuletzt gespeicherten Darstellung gerendert, können aber nicht bearbeitet werden. Für ein Vorschaufenster ist das meist richtig, denn Vorschau bedeutet ansehen, nicht ausfüllen. Wenn dasselbe Fenster aber auch als Formularausfüllfläche für vertrauenswürdige interne Dokumente dient, bauen Sie zwei Ladepfade mit einer ausdrücklichen Vertrauensentscheidung dazwischen, nicht einen Pfad mit Kompromisseinstellung. Die Formularseite dieser Aufteilung hat eigene Fallen, beschrieben in Formularfeldnavigation und Appearance-Neuerzeugung.
Links: Der Standardhandler ruft die Shell auf
Nicht abgefangene Linkklicks gehen direkt an das Betriebssystem. Die Standard-LinkOptions des Viewers enthalten loAutoOpenURI, was genau zur NTLM-Geschichte oben führt. Zwei Ereignisse bilden die Drosselstelle: OnWebLinkClick für URLs, die im Seitentext erkannt werden, und OnAnnotationLinkClick für Linkannotationen mit URI- oder Launch-Aktionen. Setzen Sie in beiden Handled := True bedingungslos, und erlauben Sie anschließend nur wieder, was Ihre Richtlinie zulässt. Entfernen Sie als Defense in Depth außerdem loAutoOpenURI aus LinkOptions für feindliche Eingaben und stellen Sie sicher, dass loAutoLaunch (standardmäßig aus) nie hineinkriecht:
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
const Url: WString; var Handled: Boolean);
begin
Handled := True; // never fall through to the default shell behavior
if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
and HostIsAllowed(Url) then
OpenInBrowser(Url)
else
FAudit.LogBlockedLink(FDocumentId, Url);
end;
Zwei Implementierungshinweise. Schemaprüfungen müssen Präfixprüfungen auf der rohen Zeichenfolge sein, bevor irgendein Parsing stattfindet, weil file://, UNC-Pfade und exotische Schemas genau die Werte sind, die naive URL-Parser abstürzen lassen oder an ihnen vorbeirutschen. Und loggen Sie jeden Block mit der Dokumentidentität. Ein Schub blockierter file://-Links über viele eingehende Dokumente hinweg ist ein Vorfallsignal, das Ihr Security-Team haben will, kein Rauschen.
Anhänge: Erweiterungsrichtlinie und der Dateiname, den Sie nicht gewählt haben
Ein PDF ist ein Container, und AttachmentCount plus die Eigenschaft AttachmentName[] sagen Ihnen, was es trägt, bevor irgendetwas die Platte berührt. Zwei getrennte Kontrollen zählen. Die offensichtliche ist die Typrichtlinie, also eine Allowlist von Erweiterungen, die jemals exportiert werden dürfen. Die subtilere ist, dass der Name des Anhangs angreifergesteuerte Daten sind: Ein eingebetteter Name wie ..\..\Startup\update.exe verwandelt einen unvorsichtigen Speichervorgang in Path Traversal. Die Komponente gibt Ihnen die Nutzlast als Bytes über Attachment[]. Ihr Code wählt den Pfad, also bauen Sie ihn aus einem bereinigten Basisnamen, nie aus der rohen eingebetteten Zeichenfolge:
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
RawName, SafeName, Ext: string;
Data: TBytes;
begin
RawName := string(Pdf.AttachmentName[Index]);
SafeName := ExtractFileName(RawName); // strips any path components
Ext := LowerCase(ExtractFileExt(SafeName));
if not FAllowedExt.Contains(Ext) then // allowlist, not blocklist
raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);
Data := Pdf.Attachment[Index]; // embedded payload as raw bytes
TFile.WriteAllBytes(
IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;
Bevorzugen Sie die Allowlist-Richtung. Eine Blocklist „gefährlicher“ Erweiterungen ist ein Rennen, das Sie an dem Tag verlieren, an dem jemand eine Erweiterung bewaffnet, von der Sie noch nie gehört haben. Eine Allowlist mit .pdf, .png und .csv fällt geschlossen aus.
Was Verschlüsselungsberechtigungen tatsächlich versprechen
Der Standard Security Handler von ISO 32000-1 kodiert Berechtigungsflags für Drucken, Kopieren von Inhalten und Änderung. Die Eigenschaften Permissions und UserPermissions stellen sie nach dem Öffnen des Dokuments als rohe Bitmasken bereit (ISO 32000-1 Tabelle 22 definiert die Bits; eine unverschlüsselte Datei meldet alle Bits gesetzt). Lesen Sie sie und beachten Sie sie in Ihrer Befehlsschicht, aber verstehen Sie ihre Natur: Bei einem Dokument, das mit Besitzerkennwort und leerem Benutzerkennwort verschlüsselt ist, wird der Inhalt beim Öffnen vollständig entschlüsselt, und die Flags sind eine Bitte an Viewer, kein Durchsetzungsmechanismus. Die Folgerung schneidet in beide Richtungen. Präsentieren Sie Berechtigungsflags Nutzern nicht als Sicherheitsmerkmal der Dokumente, die sie senden. Beachten Sie umgekehrt das Bit für Barrierefreiheits-Extraktion (Bit 10) auch dort, wo allgemeines Kopieren (Bit 5) verweigert ist. Screenreader-Zugriff ist im Berechtigungsmodell aus gutem Grund separat ausgespart.
Setzen Sie verweigerte Aktionen in der Befehlsschicht durch, nicht durch Verstecken von Toolbar-Schaltflächen. Ctrl+C, Kontextmenüs und Ziehen-Auswählen umgehen alle eine Toolbar; eine einzelne Berechtigungsprüfung im Kopierbefehl umgeht nichts.
Für Dokumente, die wirklich ein Benutzerkennwort erfordern, weisen Sie Password zu, bevor Sie Active := True setzen, und behandeln Sie den Wert wie das Geheimnis, das er ist: Holen Sie ihn pro Sitzung aus Ihrem Credential Store, halten Sie ihn aus Logs und Crashreports heraus und speichern Sie ihn nie neben dem Dokument. Ein Vorschaufenster, das Kennwörter „der Bequemlichkeit halber“ cached, ist leise zu einer Kennwortdatenbank ohne deren Schutzmaßnahmen geworden.
Drucken verdient eine eigene Entscheidung, statt die Kopierregel zu erben. Ein physischer Ausdruck ist per Definition nicht auditiert, aber Druck pauschal zu blockieren treibt Nutzer zu Screenshots, die schlechter sind. Viele Teams landen bei „Druck erlaubt, mit Wasserzeichen aus Nutzeridentität und Zeitstempel“. Setzen Sie das im Druckbefehl durch, und vergessen Sie nicht: Ein Wasserzeichen ist Abschreckung und Zuordnung, nie Prävention.
Was der Eingang Ihnen schon hätte sagen sollen
Ein Vorschaufenster trifft bessere Entscheidungen, wenn die Datei mit einem Dossier ankommt: verschlüsselt oder nicht, JavaScript vorhanden, Anhangszählung, Formulartyp. Dieser Inspektionsdurchlauf gehört vor den Viewer. Das Muster in einer PDF-Eingangsprüfungs-Workbench erzeugt genau die Flags, die eine Vorschau-Policy verbraucht. Dateien, die der Eingang als riskant markiert hat, können dann automatisch über den gehärteten Pfad öffnen, während Routinedokumente ihre Bequemlichkeiten behalten. Verbinden Sie beide über ein gemeinsames Policy-Objekt statt über zwei Konfigurationsdialoge, die ab dem zweiten Release auseinanderlaufen.
Häufig gestellte Fragen
Wie verhindere ich, dass ein PDF in meinem Delphi-Viewer JavaScript ausführt?
Laden Sie es mit FormFill := False, bevor Sie Active := True setzen; die Skriptingumgebung initialisiert nie. Der Preis: AcroForm-Felder sind für diese Sitzung schreibgeschützt.
Reichen PDF-Berechtigungsflags aus, um Kopieren oder Drucken zu verhindern?
Nein. Bei Dokumenten mit nur Besitzerkennwort sind die Flags beratend; die Durchsetzung passiert in Ihrer Befehlsschicht. Behandeln Sie die Permissions-Bitmaske als Eingabe für Ihre Richtlinie, nicht als die Richtlinie selbst.
Genügt es, gefährliche Anhangserweiterungen zu blockieren?
Verwenden Sie eine Allowlist statt einer Blocklist, bereinigen Sie den eingebetteten Dateinamen vor jedem Speichern mit ExtractFileName, und schreiben Sie Exporte nur in ein Verzeichnis, das kein Suchpfad und kein Autostartmechanismus liest.
Brauche ich einen separaten Prozess, um nicht vertrauenswürdige PDFs sicher vorzuschauen?
Für normale Geschäftseingänge ist eine In-Process-Vorschau mit deaktiviertem Skripting und abgefangenen Links eine vernünftige Schwelle. Bei anonymen öffentlichen Uploads rendern Sie in einem separaten Workerprozess mit niedrigen Rechten und schicken Bitmaps an die UI. Ein Engine-Fehler kostet Sie dann einen Worker, nicht die Anwendung.
Lizenzierung, die sicherheitsrelevante API-Oberfläche und ein Demo für einen gehärteten Viewer stehen auf der Produktseite: PDFium Component.