Article technique

Prévisualisation PDF sécurisée dans les applications Delphi avec PDFium Component

Une équipe de support que j’ai accompagnée prévisualisait les pièces jointes clients dans un volet PDF intégré. Une « facture » fabriquée contenait un lien dont le texte visible affichait https://portal.example.com, mais dont l’action pointait vers une URI file:// sur un partage UNC contrôlé par un attaquant, le type de destination que Windows résout en proposant des identifiants NTLM avant même l’ouverture d’un navigateur. Au clic, le volet a docilement délégué au shell. Pas d’exploit, pas de fichier malformé, pas de bogue moteur : seulement une visionneuse qui appliquait ses comportements par défaut à une entrée hostile. Un volet de prévisualisation dans une application métier est une décision d’exécution, et PDFium Component, une visionneuse PDF en code source pour Delphi, C++Builder et Lazarus, vous donne les points d’accroche de cette décision : options au chargement, événement d’interception des liens, appels d’accès aux pièces jointes et requêtes de permissions. Cet article parcourt la surface d’attaque dans l’ordre où le document l’atteint

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

Soyez honnête sur ce que signifie « prévisualisation sécurisée ». Le moteur de rendu analyse des octets non fiables, et son durcissement est le socle sur lequel vous vous appuyez, mais tout ce qui est au-dessus de ce socle relève de la politique applicative : les scripts s’initialisent-ils, que se passe-t-il quand l’utilisateur clique un lien, les fichiers intégrés peuvent-ils atteindre le disque, le presse-papiers et l’imprimante sont-ils des portes ou des murs. Une précision de cadrage dès le départ : le commutateur FPDF_SetSandBoxPolicy du moteur a peu d’effet pratique, car la plupart des restrictions du moteur sont intégrées ; ne fondez donc aucune partie de votre stratégie d’isolation dessus. Pour des flux réellement hostiles, par exemple un portail public de dépôt, l’isolation réelle consiste à rendre dans un processus séparé à faibles privilèges ; les indicateurs in-process sont de la politique, pas du confinement

Deux surfaces s’oublient facilement parce qu’aucun clic ne les touche jamais. Les fichiers temporaires : si votre pipeline place les documents entrants sur disque avant prévisualisation, ces copies intermédiaires survivent à la session sauf si quelque chose les supprime de façon vérifiable, et « récupérable depuis le répertoire temporaire » annule tous les contrôles que le volet lui-même applique ; préférez le chargement depuis la mémoire via TPdfStreamAdapter, afin que les octets hostiles n’obtiennent jamais leur propre chemin. Et le presse-papiers : une prévisualisation qui autorise sélectionner puis copier a déjà exporté le document, un écran à la fois

Désactiver JavaScript au chargement, pas dans l’interface

Le JavaScript de document dans PDFium Component ne s’initialise qu’avec l’environnement de remplissage des formulaires. Un chargement avec FormFill := False désactive donc le scripting à la racine au lieu d’en masquer les symptômes :

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;

Le compromis est réel et doit figurer dans votre spécification : quand le remplissage de formulaires est désactivé, les interactions AcroForm légitimes et les scripts de validation disparaissent aussi. Les champs s’affichent avec leur apparence enregistrée, mais ne peuvent pas être modifiés. Pour un volet de prévisualisation, c’est généralement correct, car prévisualiser signifie regarder, pas remplir ; mais si la même fenêtre sert aussi de surface de saisie pour des documents internes fiables, créez deux chemins de chargement avec une décision de confiance explicite entre les deux, pas un seul chemin avec un réglage de compromis. Le côté formulaire de cette séparation a ses propres pièges, couverts dans la navigation dans les champs de formulaire et la régénération d’apparence

Liens : le gestionnaire par défaut délègue au shell

Les clics sur liens non traités vont directement au système d’exploitation : les LinkOptions par défaut de la visionneuse incluent loAutoOpenURI, ce qui correspond exactement au scénario NTLM ci-dessus. Deux événements forment le point d’étranglement : OnWebLinkClick pour les URL détectées dans le texte de page, et OnAnnotationLinkClick pour les annotations de lien qui portent des actions URI ou de lancement. Dans les deux cas, définissez Handled := True sans condition, puis ne réautorisez que ce que la politique permet ; et, en défense en profondeur, retirez loAutoOpenURI de LinkOptions pour les entrées hostiles et vérifiez que loAutoLaunch, désactivé par défaut, ne réapparaisse jamais :

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;

Deux notes d’implémentation. Les contrôles de schéma doivent être des tests de préfixe sur la chaîne brute avant toute analyse, parce que file://, les chemins UNC et les schémas exotiques sont précisément les valeurs qui font planter les parseurs d’URL naïfs ou passent au travers. Et journalisez chaque blocage avec l’identité du document : une rafale de liens file:// bloqués dans de nombreux documents entrants est un signal d’incident que votre équipe sécurité veut voir, pas du bruit

Pièces jointes : politique d’extension et nom de fichier que vous n’avez pas choisi

Un PDF est un conteneur, et AttachmentCount avec la propriété AttachmentName[] vous indiquent ce qu’il transporte avant que quoi que ce soit ne touche le disque. Deux contrôles distincts comptent. Le plus évident est la politique de type, c’est-à-dire une liste d’extensions autorisées pouvant être exportées. Le plus subtil est que le nom de la pièce jointe est une donnée contrôlée par l’attaquant : un nom intégré comme ..\..\Startup\update.exe transforme une sauvegarde imprudente en traversée de chemins. Le composant vous remet la charge utile sous forme d’octets via Attachment[] ; votre code choisit le chemin, donc construisez-le à partir d’un nom de base assaini, jamais à partir de la chaîne intégrée brute :

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;

Préférez l’approche par liste d’autorisation. Une liste de blocage d’extensions « dangereuses » est une course perdue le jour où quelqu’un militarise une extension dont vous n’avez jamais entendu parler ; une liste d’autorisation limitée à .pdf, .png et .csv échoue fermée

Ce que promettent réellement les permissions de chiffrement

Le gestionnaire de sécurité standard d’ISO 32000-1 encode des indicateurs de permission, impression, copie de contenu, modification, que les propriétés Permissions et UserPermissions exposent sous forme de masques de bits bruts une fois le document ouvert (la table 22 d’ISO 32000-1 définit les bits ; un fichier non chiffré les déclare tous actifs). Lisez-les et respectez-les dans votre couche de commandes, mais comprenez leur nature : pour un document chiffré avec un mot de passe propriétaire et un mot de passe utilisateur vide, le contenu est entièrement déchiffré à l’ouverture, et les indicateurs sont une demande adressée aux visionneuses plutôt qu’un mécanisme d’application. La conséquence va dans les deux sens. Ne présentez pas les indicateurs de permission aux utilisateurs comme une fonctionnalité de sécurité des documents qu’ils envoient ; et, inversement, respectez le bit d’extraction pour l’accessibilité (bit 10) même lorsque la copie générale (bit 5) est refusée, car l’accès par lecteur d’écran est isolé séparément dans le modèle de permissions pour une bonne raison

Appliquez les actions refusées au niveau de la commande, pas en masquant des boutons de barre d’outils. Ctrl+C, les menus contextuels et la sélection par glisser-déposer contournent tous une barre d’outils ; un contrôle unique dans la commande de copie ne contourne rien

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

L’impression mérite sa propre décision au lieu d’hériter de la règle de copie. Une sortie papier n’est pas auditable par définition, mais bloquer totalement l’impression pousse les utilisateurs vers des captures d’écran, ce qui est pire. Beaucoup d’équipes finissent sur « impression autorisée, avec filigrane d’identité utilisateur et horodatage » ; appliquez-le dans la commande d’impression, et souvenez-vous qu’un filigrane est une dissuasion et une attribution, jamais une prévention

Ce que l’admission du document aurait déjà dû vous dire

Un volet de prévisualisation prend de meilleures décisions quand le fichier arrive avec un dossier : chiffré ou non, présence de JavaScript, inventaire des pièces jointes, type de formulaire. Cette passe d’inspection doit vivre en amont de la visionneuse ; le modèle de construction d’un poste de revue d’admission PDF produit exactement les indicateurs consommés par une politique de prévisualisation. Les fichiers marqués risqués à l’admission peuvent alors s’ouvrir automatiquement par le chemin renforcé, tandis que les documents ordinaires conservent leurs commodités. Reliez les deux avec un seul objet de politique partagé plutôt qu’avec deux écrans de configuration qui divergeront dès la deuxième version

Questions fréquentes

Comment empêcher un PDF d’exécuter JavaScript dans ma visionneuse Delphi ?

Chargez-le avec FormFill := False avant Active := True ; l’environnement de scripting ne s’initialise jamais. Le coût : les champs AcroForm sont en lecture seule pendant cette session

Les indicateurs de permission PDF suffisent-ils à empêcher la copie ou l’impression ?

Non. Pour les documents protégés uniquement par mot de passe propriétaire, les indicateurs sont consultatifs ; l’application réelle se fait dans votre couche de commandes. Traitez le masque Permissions comme une entrée de votre politique, pas comme la politique elle-même

Bloquer les extensions de pièces jointes dangereuses suffit-il ?

Utilisez une liste d’autorisation plutôt qu’une liste de blocage, assainissez le nom de fichier intégré avec ExtractFileName avant toute sauvegarde, et n’écrivez les exports que dans un répertoire qu’aucun chemin de recherche ni mécanisme de démarrage automatique ne lit

Ai-je besoin d’un processus séparé pour prévisualiser des PDF non fiables en sécurité ?

Pour une admission métier ordinaire, une prévisualisation in-process avec scripting désactivé et liens interceptés constitue un niveau raisonnable. Pour des dépôts publics anonymes, rendez dans un processus worker séparé à faibles privilèges et envoyez des bitmaps à l’interface ; une faille moteur vous coûte alors un worker, pas l’application

La licence, la surface API liée à la sécurité et une démo de visionneuse renforcée sont sur la page produit : PDFium Component