Technischer Artikel

Sichere PDF-Vorschau in Delphi mit PDFium Component

Eine nicht vertrauenswürdige PDF-Datei in der eigenen Anwendung anzuzeigen ist eine Entscheidung über Codeausführung, und worauf es ankommt, ist nicht die Oberfläche des Viewers, sondern was der Anzeigebereich von sich aus verweigert. Schreiben Sie die Datei nicht auf die Festplatte. Lassen Sie ihre Links nichts an die Shell weiterreichen. Geben Sie ihren Anhängen keinen Pfad. Der meiste Schaden durch ein feindseliges Dokument entsteht nicht durch einen Exploit in der Engine, sondern dadurch, dass ein Viewer vollkommen alltägliche Dinge mit vom Angreifer gelieferten Eingaben tut: einen file://-Link auf eine UNC-Freigabe öffnen, die NTLM-Anmeldedaten preisgibt, eine zwischengespeicherte Kopie im Temp-Verzeichnis liegen lassen, eingebettete Nutzdaten dorthin kopieren, wohin eine Dateinamenszeichenkette zeigt. PDFium Component ist ein PDF-Viewer im Quellcode für Delphi, C++Builder und Lazarus, und die Komponente legt die relevanten Schalter dorthin, wo Sie sie erreichen: ein Flag beim Laden, das Skripting abschaltet, Linkklick-Ereignisse, denen Sie widersprechen können, Anhangzugriff, der durch Ihren eigenen Code läuft, und Berechtigungsbits, die Sie auslesen können. Die folgende Reihenfolge begleitet ein Dokument von dem Moment an, in dem es eintrifft, bis zu dem Moment, in dem ein Benutzer darin auf etwas klickt

Das Bedrohungsmodell eines Vorschaubereichs

Seien Sie ehrlich darüber, was Ihnen „sichere Vorschau“ einbringt. Der Renderer parst nicht vertrauenswürdige Bytes, was immer Sie tun, und die Härtung der Engine selbst ist der Boden, auf dem Sie stehen. Alles oberhalb dieses Bodens ist Anwendungsrichtlinie: ob Skripte initialisiert werden, was ein Linkklick auslöst, ob eingebettete Dateien auf die Festplatte gelangen können, ob Zwischenablage und Drucker Türen oder Wände sind. Eines sollten Sie früh abschreiben, nämlich den Schalter FPDF_SetSandBoxPolicy der Engine. Die meisten Einschränkungen der Engine sind einkompiliert, der Schalter ändert in der Praxis wenig, und einen Teil Ihrer Isolationsgeschichte darauf zu verbuchen erzeugt nur das trügerische Gefühl, etwas getan zu haben. Wenn die Eingabe wirklich feindselig ist, etwa bei einem öffentlichen Upload-Portal, besteht die einzige echte Isolation darin, in einem separaten Prozess mit geringen Rechten zu rendern und Bitmaps an die Oberfläche zu liefern. Flags im selben Prozess sind Richtlinie. Sie sind keine Einschließung

PDFium-Component-Diagramm, das Richtlinienschalter für die PDF-Vorschau im selben Prozess dem prozessexternen Rendern in einem Worker mit geringen Rechten für feindselige Dokumente gegenüberstellt
Flags im selben Prozess legen die Latte für bekannte Absender höher, während anonyme Uploads einen separaten Worker mit geringen Rechten rechtfertigen, der nur Bitmaps an die Oberfläche liefert

Zwei Angriffsflächen vergisst man leicht, gerade weil sie nie ein Klick berührt. Die erste sind temporäre Dateien. Wenn Ihre Verarbeitungskette eingehende Dokumente vor der Vorschau auf der Festplatte zwischenlagert, überleben diese Kopien die Sitzung, sofern nicht etwas sie nachweislich löscht, und eine Datei, die „aus dem Temp-Verzeichnis wiederherstellbar“ ist, hat still und leise jede Kontrolle ausgehebelt, die der Anzeigebereich selbst durchsetzt. Laden Sie stattdessen über TPdfStreamAdapter aus dem Speicher, damit die feindseligen Bytes nie einen eigenen Pfad bekommen. Die zweite ist die Zwischenablage. Eine Vorschau, die Markieren und Kopieren erlaubt, hat das Dokument bereits exportiert, jeweils einen Bildschirm voll, und keine Linkabfangung wird das bemerken

JavaScript beim Laden abschalten, nicht in der Oberfläche

Dokument-JavaScript initialisiert sich in PDFium Component nur zusammen mit der Formularausfüllumgebung. Das Laden mit FormFill := False deaktiviert Skripting deshalb an der Wurzel, statt nur seine Symptome zu unterdrücken:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // Keine Formularumgebung, also auch keine JavaScript-Engine
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // Rohes Flag-Wort; alle Bits gesetzt = keine Einschränkung
end;

Der Zielkonflikt ist real und gehört in Ihre Spezifikation. Mit abgeschaltetem Formularausfüllen sind auch die legitime AcroForm-Interaktion und die Validierungsskripte verschwunden; Felder werden mit ihrem zuletzt gespeicherten Erscheinungsbild dargestellt, lassen sich aber nicht bearbeiten. Für einen Vorschaubereich ist das meist die richtige Entscheidung, denn Vorschau heißt ansehen, nicht ausfüllen. Wenn dasselbe Fenster aber zusätzlich als Ausfülloberfläche für vertrauenswürdige interne Dokumente dient, lautet die Antwort: zwei Ladepfade mit einer expliziten Vertrauensentscheidung dazwischen, nicht ein Pfad mit einer Kompromisseinstellung, die für den feindseligen Fall zu locker und für den vertrauenswürdigen zu streng ist. Die Ausfüllseite dieser Trennung hat eigene Fallstricke, behandelt in Formularfeld-Navigation und Regenerierung des Erscheinungsbilds

Links: Der Standardhandler reicht an die Shell weiter

Sich selbst überlassen gehen Linkklicks direkt an das Betriebssystem. Die Standardwerte von LinkOptions im Viewer enthalten loAutoOpenURI, und das ist genau das file://-auf-UNC-Freigabe-Leck, das nur darauf wartet zu passieren. Zwei Ereignisse bilden den Engpass: OnWebLinkClick für im Seitentext erkannte URLs und OnAnnotationLinkClick für Link-Annotationen mit URI- oder Launch-Aktionen. Setzen Sie in beiden bedingungslos Handled := True, bevor Sie irgendetwas entscheiden, und geben Sie danach nur das wieder frei, was die Richtlinie erlaubt. Als zweite Schicht entfernen Sie loAutoOpenURI für feindselige Eingaben aus LinkOptions und stellen sicher, dass loAutoLaunch, standardmäßig aus, sich nicht über eine kopierte Konfiguration wieder einschleicht:

Ablaufdiagramm des Abfangens von PDF-Linkklicks in einem Delphi-Vorschaubereich mit Präfixprüfung des Schemas auf der Rohzeichenkette und Audit-Protokollierung blockierter Links
Handled in beiden Link-Ereignissen zu setzen hält jeden Klick unter der Anwendungsrichtlinie, und eine Präfixprüfung auf der Rohzeichenkette hält file:// und UNC-Schemata draußen
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // Niemals auf das Standardverhalten der Shell durchfallen

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

Zwei Details entscheiden darüber, ob das tatsächlich hält. Erstens muss die Schemaprüfung eine Präfixprüfung auf der Rohzeichenkette vor jeglichem Parsen sein, denn file://, UNC-Pfade und exotische Schemata sind genau die Werte, die einen naiven URL-Parser zum Absturz bringen oder durch einen rutschen, der zu eifrig normalisiert. Zweitens protokollieren Sie jede Blockade mit angehängter Dokumentidentität. Eine Handvoll blockierter file://-Links ist Hintergrundrauschen; ein Schwall davon über viele eingehende Dokumente in kurzer Zeit ist ein Vorfall, von dem Ihr Sicherheitsteam lieber von Ihnen als von anderswo erfährt

Anhänge: Erweiterungsrichtlinie und der Dateiname, den Sie nicht gewählt haben

Ein PDF ist ein Container, und AttachmentCount zusammen mit der Eigenschaft AttachmentName[] sagt Ihnen, was es transportiert, bevor irgendetwas die Festplatte berührt. Hier zählen zwei getrennte Kontrollen, und nur eine davon liegt auf der Hand. Die offensichtliche ist die Typrichtlinie: eine Allowlist von Erweiterungen, die überhaupt jemals exportiert werden dürfen. Die subtile ist, dass der Name des Anhangs vom Angreifer kontrollierte Daten sind, Punkt. Ein eingebetteter Name wie ..\..\Startup\update.exe macht aus einem unachtsamen Speichern einen Path-Traversal, der eine ausführbare Datei in einen Ordner legt, den Windows bei der Anmeldung startet. Die Komponente gibt Ihnen die Nutzdaten über Attachment[] als Bytes und überlässt die Pfadwahl Ihrem Code, also bauen Sie diesen Pfad aus einem bereinigten Basisnamen und nie aus der rohen eingebetteten Zeichenkette:

PDFium-Component-Ablaufdiagramm, das einen vom Angreifer kontrollierten PDF-Anhangnamen über ExtractFileName und eine Allowlist von Erweiterungen bereinigt, bevor Bytes geschrieben werden
Der eingebettete Anhangname ist Angreifereingabe, also wird der Exportpfad aus einem bereinigten Basisnamen neu gebaut und durch eine standardmäßig sperrende Allowlist von Erweiterungen abgesichert
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // Entfernt alle Pfadbestandteile
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // Allowlist, keine Blocklist
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // Eingebettete Nutzdaten als rohe Bytes
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Bevorzugen Sie die Allowlist-Richtung. Eine Blocklist „gefährlicher“ Erweiterungen ist ein Wettlauf, den Sie an dem Tag verlieren, an dem jemand eine Erweiterung bewaffnet, von der Sie nie gehört haben; eine Allowlist aus .pdf, .png und .csv sperrt im Zweifel

Was Verschlüsselungsberechtigungen tatsächlich versprechen

Der Standard-Sicherheitshandler von ISO 32000-1 kodiert Berechtigungs-Flags für Drucken, Inhaltskopieren und Ändern, und die Eigenschaften Permissions und UserPermissions legen sie nach dem Öffnen des Dokuments als rohe Bitmasken offen. ISO 32000-1 Tabelle 22 definiert die Bits, und eine unverschlüsselte Datei meldet alle Bits gesetzt. Lesen Sie sie und halten Sie sich in Ihrer Befehlsschicht daran, aber seien Sie sich im Klaren darüber, was sie sind. Bei einem Dokument, das mit einem Owner-Passwort und leerem User-Passwort verschlüsselt ist, wird der Inhalt beim Öffnen vollständig entschlüsselt, und die Flags sind eine Bitte an konforme Viewer, kein Durchsetzungsmechanismus. Daraus folgen zwei Dinge, und sie ziehen in entgegengesetzte Richtungen. Präsentieren Sie Benutzern Berechtigungs-Flags niemals als Sicherheitseigenschaft der Dokumente, die sie erhalten, denn das sind sie nicht. Halten Sie zugleich das Bit für Extraktion zu Barrierefreiheitszwecken (Bit 10) ein, auch wo allgemeines Kopieren (Bit 5) verweigert wird; der Zugriff für Screenreader ist im Berechtigungsmodell absichtlich gesondert herausgenommen, und ihn zu streichen, weil „Kopieren aus ist“, zerstört assistive Technik ohne jeden Sicherheitsgewinn

Setzen Sie verweigerte Aktionen auf Befehlsebene durch, nicht durch Ausblenden von Symbolleistenschaltflächen. Strg+C, Kontextmenüs und Ziehen zum Markieren umgehen alle eine Symbolleiste; eine einzige Berechtigungsprüfung innerhalb des Kopierbefehls umgeht nichts

Für Dokumente, die tatsächlich ein User-Passwort verlangen, weisen Sie Password vor Active := True zu und behandeln den Wert wie das Geheimnis, das er ist: Holen Sie ihn pro Sitzung aus Ihrem Anmeldedatenspeicher, halten Sie ihn aus Protokollen und Absturzberichten heraus und speichern Sie ihn nie neben dem Dokument. Ein Vorschaubereich, der Passwörter „der Bequemlichkeit halber“ zwischenspeichert, ist still und leise zu einer Passwortdatenbank geworden, ohne deren Schutzmaßnahmen zu besitzen

Das Drucken verdient eine eigene Entscheidung, statt zu erben, worauf die Kopierregel gerade hinauslief. Ein physischer Ausdruck ist per Definition nicht prüfbar, doch das Drucken rundheraus zu blockieren treibt Benutzer eher zu Screenshots, die auf jeder Achse schlechter sind. Ein üblicher Mittelweg ist, das Drucken zu erlauben, aber jede Seite mit der Identität des Benutzers und einem Zeitstempel zu versehen, durchgesetzt innerhalb des Druckbefehls. Halten Sie nur die richtige Erwartung daran: Ein Wasserzeichen ist Abschreckung und Zuordnung. Es ist keine Verhinderung

Was die Eingangsprüfung Ihnen längst gesagt haben sollte

Ein Vorschaubereich trifft bessere Entscheidungen, wenn die Datei bereits mit einem Dossier eintrifft: verschlüsselt oder nicht, JavaScript vorhanden oder nicht, eine Bestandsaufnahme der Anhänge, der Formulartyp. Dieser Prüfdurchlauf gehört dem Viewer vorgelagert, und das Muster in der Werkbank zur PDF-Eingangsprüfung erzeugt genau die Flags, die eine Vorschaurichtlinie verarbeiten will. Dateien, die die Eingangsprüfung als riskant markiert hat, öffnen automatisch über den gehärteten Pfad; Routinedokumente behalten ihre Annehmlichkeiten. Verbinden Sie die beiden Stufen mit einem gemeinsamen Richtlinienobjekt statt mit zwei Konfigurationsseiten, die spätestens ab der zweiten Version auseinanderdriften, egal wie sorgfältig Sie sie beim ersten Mal schreiben

Wo die Grenze zwischen prozessintern und prozessextern verläuft, hängt davon ab, wer Ihnen Dateien schickt. Bei gewöhnlichem Geschäftseingang sind die Absender bekannt und lediglich unachtsam, und eine prozessinterne Vorschau mit abgeschaltetem Skripting und abgefangenen Links ist eine vertretbare Latte. Bei anonymen öffentlichen Uploads ist sie das nicht, und keine Menge prozessinterner Flags macht sie dazu; rendern Sie diese in einem separaten Worker mit geringen Rechten und liefern Sie Bitmaps an die Oberfläche, damit ein Fehler in der Engine Sie einen Worker kostet und nicht die Hostanwendung. Treffen Sie diese Aufteilung bewusst und schreiben Sie auf, in welchen Topf jeder Eingangspfad fällt, denn die Kosten einer falschen Vermutung sind asymmetrisch

Lizenzierung, die sicherheitsrelevante API-Oberfläche und eine Demo eines gehärteten Viewers finden Sie auf der Produktseite: PDFium Component