Article technique

Aperçu PDF sécurisé en Delphi avec PDFium Component

Prévisualiser un PDF non fiable dans votre propre application est une décision d'exécution, et ce qui compte n'est pas l'habillage du visualiseur mais ce que le volet refuse de faire de lui-même. N'écrivez pas le fichier sur le disque. Ne laissez pas ses liens appeler le shell. Ne donnez pas de chemin à ses pièces jointes. L'essentiel des dégâts causés par un document hostile ne vient pas d'une faille du moteur mais d'un visualiseur qui fait des choses parfaitement ordinaires avec une entrée fournie par l'attaquant : ouvrir un lien file:// vers un partage UNC qui fuite des identifiants NTLM, laisser une copie de travail dans le répertoire temporaire, copier des charges utiles embarquées là où une chaîne de nom de fichier le lui indique. PDFium Component est un visualiseur PDF livré en code source pour Delphi, C++Builder et Lazarus, et il place les interrupteurs pertinents à votre portée : un indicateur de chargement qui tue les scripts, des événements de clic sur lien auxquels vous pouvez opposer votre veto, un accès aux pièces jointes qui passe par votre propre code, et des bits de permission que vous pouvez lire. L'ordre ci-dessous suit un document depuis son arrivée jusqu'au moment où un utilisateur clique sur quelque chose dedans

Le modèle de menace d'un volet de prévisualisation

Soyez honnête sur ce que vous achète un « aperçu sécurisé ». Le moteur de rendu analyse des octets non fiables quoi que vous fassiez, et le durcissement propre au moteur est le plancher sur lequel vous vous tenez. Tout ce qui se trouve au-dessus de ce plancher relève de la politique applicative : si les scripts s'initialisent, ce que fait un clic sur un lien, si les fichiers embarqués peuvent atteindre le disque, si le presse-papiers et l'imprimante sont des portes ou des murs. Une chose à écarter tôt est l'interrupteur FPDF_SetSandBoxPolicy du moteur. La plupart des restrictions du moteur sont compilées en dur, l'interrupteur change peu de choses en pratique, et lui attribuer une part de votre stratégie d'isolation ne produit qu'un faux sentiment d'avoir agi. Quand l'entrée est véritablement hostile, par exemple un portail de dépôt public, la seule isolation réelle consiste à rendre dans un processus séparé à faibles privilèges et à envoyer des bitmaps à l'interface. Les indicateurs intra-processus sont de la politique. Ils ne sont pas du confinement

Schéma PDFium Component opposant les interrupteurs de politique d'aperçu PDF intra-processus au rendu hors processus dans un worker à faibles privilèges pour les documents hostiles
Les indicateurs intra-processus relèvent le niveau pour des expéditeurs connus, tandis que les dépôts anonymes justifient un worker séparé à faibles privilèges qui n'envoie que des bitmaps à l'interface

Deux surfaces sont faciles à oublier précisément parce qu'aucun clic ne les touche jamais. La première, ce sont les fichiers temporaires. Si votre chaîne de traitement écrit les documents entrants sur le disque avant l'aperçu, ces copies survivent à la session à moins que quelque chose ne les supprime de façon vérifiable, et un fichier « récupérable dans le répertoire temporaire » a discrètement mis en échec tous les contrôles que le volet lui-même applique. Chargez plutôt depuis la mémoire via TPdfStreamAdapter, pour que les octets hostiles n'obtiennent jamais de chemin à eux. La seconde, c'est le presse-papiers. Un aperçu qui autorise la sélection et la copie a déjà exporté le document, un écran à la fois, et aucune interception de lien n'attrapera cela

Tuez JavaScript au chargement, pas dans l'interface

Dans PDFium Component, le JavaScript de document ne s'initialise qu'avec l'environnement de remplissage de formulaires. Charger avec FormFill := False désactive donc les scripts à la racine au lieu d'en supprimer les symptômes :

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // pas d'environnement de formulaire, donc pas de moteur JavaScript
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // mot d'indicateurs brut ; tous les bits à 1 = aucune restriction
end;

Le compromis est réel et doit figurer dans votre spécification. Avec le remplissage de formulaires désactivé, l'interaction AcroForm légitime et les scripts de validation disparaissent aussi ; les champs s'affichent avec leur dernière apparence enregistrée mais ne peuvent pas être modifiés. Pour un volet de prévisualisation, c'est généralement le bon choix, puisque prévisualiser veut dire regarder, pas remplir. Mais si la même fenêtre sert aussi de surface de remplissage pour des documents internes de confiance, la réponse tient en deux chemins de chargement séparés par une décision de confiance explicite, et non en un chemin unique doté d'un réglage de compromis trop permissif pour le cas hostile et trop strict pour le cas de confiance. Le versant remplissage de ce partage a ses propres pièges, décrits dans la navigation entre champs de formulaire et la régénération des apparences

Liens : le gestionnaire par défaut appelle le shell

Laissés à eux-mêmes, les clics sur les liens vont droit au système d'exploitation. Les LinkOptions par défaut du visualiseur incluent loAutoOpenURI, ce qui constitue la fuite file:// vers un partage UNC en attente de se produire. Deux événements forment le point de passage obligé : OnWebLinkClick pour les URL détectées dans le texte des pages, et OnAnnotationLinkClick pour les annotations de lien portant des actions URI ou launch. Mettez Handled := True dans les deux, sans condition, avant de décider quoi que ce soit, puis réautorisez seulement ce que la politique permet. En seconde couche, retirez loAutoOpenURI de LinkOptions pour les entrées hostiles et assurez-vous que loAutoLaunch, désactivé par défaut, ne revienne jamais par une configuration copiée :

Schéma de flux de l'interception des clics sur liens PDF dans un volet de prévisualisation Delphi avec vérification du préfixe de schéma sur la chaîne brute et journalisation d'audit des liens bloqués
Définir Handled dans les deux événements de lien maintient chaque clic sous la politique applicative, et une vérification de préfixe sur la chaîne brute écarte les schémas file:// et UNC
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // ne jamais retomber sur le comportement shell par défaut

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

Deux détails décident si cela tient vraiment. D'abord, la vérification du schéma doit être une vérification de préfixe sur la chaîne brute avant toute analyse, car file://, les chemins UNC et les schémas exotiques sont exactement les valeurs qui font planter un analyseur d'URL naïf ou qui passent au travers d'un analyseur qui normalise trop volontiers. Ensuite, journalisez chaque blocage avec l'identité du document attachée. Une poignée de liens file:// bloqués est du bruit de fond ; une rafale de ces liens répartie sur de nombreux documents entrants en peu de temps est un incident que votre équipe sécurité préférerait apprendre de vous plutôt que d'ailleurs

Pièces jointes : politique d'extensions et le nom de fichier que vous n'avez pas choisi

Un PDF est un conteneur, et AttachmentCount avec la propriété AttachmentName[] vous dit ce qu'il transporte avant que quoi que ce soit ne touche le disque. Deux contrôles distincts comptent ici, et un seul est évident. L'évident est la politique de type : une liste d'autorisation des extensions qui peuvent être exportées. Le subtil est que le nom de la pièce jointe est une donnée contrôlée par l'attaquant, point final. Un nom embarqué comme ..\..\Startup\update.exe transforme un enregistrement négligent en traversée de chemin qui dépose un exécutable dans un dossier que Windows exécute à l'ouverture de session. Le composant vous remet la charge utile sous forme d'octets via Attachment[] et laisse votre code choisir le chemin, alors construisez ce chemin à partir d'un nom de base assaini et jamais à partir de la chaîne embarquée brute :

Schéma de pipeline PDFium Component assainissant un nom de pièce jointe PDF contrôlé par l'attaquant via ExtractFileName et une liste d'autorisation d'extensions avant l'écriture des octets
Le nom de pièce jointe embarqué est une entrée de l'attaquant, donc le chemin d'export est reconstruit à partir d'un nom de base assaini et filtré par une liste d'autorisation d'extensions qui échoue en mode fermé
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // supprime tout composant de chemin
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // liste d'autorisation, pas liste de blocage
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // charge utile embarquée en octets bruts
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Préférez le sens de la liste d'autorisation. Une liste de blocage des extensions « dangereuses » est une course que vous perdez le jour où quelqu'un militarise une extension dont vous n'aviez jamais entendu parler ; une liste d'autorisation de .pdf, .png et .csv échoue en mode fermé

Ce que les permissions de chiffrement promettent réellement

Le gestionnaire de sécurité standard de la norme ISO 32000-1 encode des indicateurs de permission pour l'impression, la copie de contenu et la modification, et les propriétés Permissions et UserPermissions les exposent sous forme de masques de bits bruts dès l'ouverture du document. Le tableau 22 de la norme ISO 32000-1 définit ces bits, et un fichier non chiffré rapporte tous les bits à 1. Lisez-les et respectez-les dans votre couche de commandes, mais soyez clair sur leur nature. Pour un document chiffré avec un mot de passe propriétaire et un mot de passe utilisateur vide, le contenu se déchiffre entièrement à l'ouverture, et les indicateurs sont une requête adressée aux visualiseurs conformes, pas un mécanisme d'application. Cela a deux conséquences, et elles tirent en sens opposés. Ne présentez jamais les indicateurs de permission aux utilisateurs comme une propriété de sécurité des documents qu'ils reçoivent, car ils n'en sont pas une. En même temps, respectez le bit d'extraction pour l'accessibilité (bit 10) même là où la copie générale (bit 5) est refusée ; l'accès des lecteurs d'écran est délibérément isolé dans le modèle de permissions, et le retirer parce que « la copie est désactivée » casse les technologies d'assistance sans aucun gain de sécurité

Appliquez les actions refusées au niveau des commandes, pas en masquant des boutons de barre d'outils. Ctrl+C, les menus contextuels et la sélection par glisser contournent tous une barre d'outils ; une seule vérification de permission à l'intérieur de la commande de copie ne contourne rien

Pour les documents qui exigent un mot de passe utilisateur, affectez Password avant Active := True et traitez la valeur comme le secret qu'elle est : récupérez-la depuis votre coffre d'identifiants pour chaque session, tenez-la hors des journaux et des rapports de plantage, et ne la conservez jamais à côté du document. Un volet de prévisualisation qui met en cache les mots de passe « par commodité » est discrètement devenu une base de mots de passe sans aucune des protections qui vont avec

L'impression mérite sa propre décision plutôt que d'hériter de la règle retenue pour la copie. Une impression papier est par définition non auditée, mais bloquer purement l'impression tend à pousser les utilisateurs vers les captures d'écran, qui sont pires sur tous les plans. Un terrain d'entente courant consiste à autoriser l'impression tout en tamponnant chaque page avec l'identité de l'utilisateur et un horodatage, appliqué à l'intérieur de la commande d'impression. Gardez seulement la bonne attente à son sujet : un filigrane relève de la dissuasion et de l'attribution. Ce n'est pas de la prévention

Ce que la réception aurait déjà dû vous dire

Un volet de prévisualisation prend de meilleures décisions quand le fichier se présente avec un dossier déjà attaché : chiffré ou non, JavaScript présent ou absent, un recensement des pièces jointes, le type de formulaire. Cette passe d'inspection se situe en amont du visualiseur, et le modèle décrit dans la construction d'un atelier de réception et de revue PDF produit exactement les indicateurs qu'une politique d'aperçu veut consommer. Les fichiers que la réception a marqués comme risqués s'ouvrent automatiquement par le chemin durci ; les documents de routine gardent leurs commodités. Reliez les deux étapes à un unique objet de politique partagé plutôt qu'à deux écrans de configuration, qui divergeront dès la deuxième version quelle que soit la rigueur de leur rédaction initiale

L'endroit où passe la frontière entre intra-processus et hors processus dépend de qui vous envoie des fichiers. Pour une réception de documents professionnels ordinaire, les expéditeurs sont connus et simplement négligents, et un aperçu intra-processus avec les scripts désactivés et les liens interceptés constitue un niveau défendable. Pour des dépôts publics anonymes, ce n'est pas le cas, et aucune quantité d'indicateurs intra-processus n'y changera rien ; rendez ces documents dans un worker séparé à faibles privilèges et envoyez des bitmaps à l'interface, pour qu'une faille du moteur vous coûte un worker plutôt que l'application hôte. Décidez ce partage délibérément et notez dans quelle catégorie tombe chaque canal d'entrée, car le coût d'une mauvaise supposition est asymétrique

Les licences, la surface d'API liée à la sécurité et une démonstration de visualiseur durci se trouvent sur la page produit : PDFium Component