Technical Article

Audit des risques de sécurité PDF avec PDFium dans Delphi

Un PDF n'est pas simplement du papier. C'est un conteneur qui peut transporter des scripts s'exécutant à l'ouverture du fichier, des liens lançant des programmes externes, des liens ciblant des serveurs web, des fichiers imbriqués, et une signature attestant que le document n'a pas été modifié depuis sa certification. Lorsqu'un fichier provient d'une source non contrôlée, la première mesure de sécurité n'est pas de l'afficher. C'est de lire ce que le fichier indique sur lui-même et de dresser l'inventaire de tout ce qu'il pourrait tenter de faire, afin qu'un humain décide si ce document a sa place dans votre flux de travail.

Cet article présente une passe d'audit statique et en lecture seule sur cette surface de risque à l'aide du composant PDFium pour Delphi et Lazarus. L'audit ne dessine jamais aucune page. Il analyse la structure du document, énumère les parties du fichier porteuses de comportements et rédige un rapport simple. C'est la différence entre demander à un inconnu de vider ses poches à l'entrée et lui faire confiance simplement parce qu'il a souri.

Ce qu'est un audit, et ce qu'il n'est pas

Soyons clairs sur la limite. Un aperçu en bac à sable (sandboxed) affiche un fichier sous des restrictions strictes afin qu'un utilisateur puisse le consulter sans que le fichier n'interagisse avec le reste de la machine. L'audit intervient en amont. C'est une inspection sans rendu dont l'unique sortie est une description de la surface de menace : quels scripts existent, quelles actions sont liées aux liens, si le fichier est signé et avec quel degré de verrouillage, et quels éléments sont joints. Vous l'exécutez lorsqu'un document franchit une limite de confiance (réception d'un e-mail, formulaire de téléversement ou flux partenaire) avant toute ouverture réelle.

Le composant charge un document de la même manière pour un audit que pour tout autre usage. Vous définissez le nom du fichier et l'activez, ce qui analyse les données de référence croisée et le catalogue du document sans afficher de page. Tout ce qui suit est lu à partir de cet état chargé et sans rendu.

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Incoming_Invoice.pdf';
    Pdf.Active := True;          // parses structure, renders nothing
    // audit the loaded document here
  finally
    Pdf.Free;
  end;
end;

JavaScript de document dans l'arbre des noms

La première chose à énumérer est le code. Un PDF peut contenir du JavaScript au niveau du document : des scripts qui ne sont attachés à aucune page ou champ mais au document lui-même, stockés dans l'arbre /Names sous une entrée /JavaScript. Un lecteur conforme les exécute à l'ouverture. C'est le mécanisme à l'origine d'une longue lignée de logiciels malveillants PDF, car il permet à un fichier d'exécuter une logique à l'instant même où l'utilisateur double-clique dessus, avant même qu'il n'ait lu le premier mot.

Un auditeur veut deux faits concernant chacun de ces scripts : son existence et son contenu. Le composant expose le nombre et vous permet de lire chaque action sous forme d'enregistrement contenant le nom du script et son corps complet. Lire le corps est important. Un script nommé Doc.0 ne vous apprend rien, mais son texte peut appeler app.launchURL ou assembler une chaîne et la transmettre là où elle ne devrait pas aller. Extraire le code source pour qu'un réviseur puisse le lire est tout l'intérêt de signaler un fichier qui exécute du code à l'ouverture.

var
  I: Integer;
  Action: TPdfJavaScriptAction;
begin
  if Pdf.JavaScriptActionCount > 0 then
    WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
            ' script(s) on open');
  for I := 0 to Pdf.JavaScriptActionCount - 1 do
  begin
    Action := Pdf.JavaScriptAction[I];
    WriteLn('  script "', Action.Name, '":');
    WriteLn(Action.Script);   // full body, for a human to read
  end;
end;

Un fichier sans aucun script de document n'est pas automatiquement sûr, car des scripts de page et de champ existent également, mais un fichier contenant des scripts de document mérite toujours un examen attentif. Le simple nombre de présence constitue un filtre utile, et le corps du script est ce qui permet de transformer ce filtre en jugement.

Actions de lancement et URI

Le comportement suivant à inventorier réside sur les liens et les annotations. Deux types d'actions importent le plus pour un auditeur. Une action de lancement (Launch action) démarre un programme externe ou ouvre un fichier local lorsque le lien est activé. Une action URI ouvre une cible web. Un réviseur examinant un document suspect doit pouvoir constater, sans cliquer, qu'un bouton de la page trois est configuré pour lancer cmd.exe ou pour ouvrir une URL ne correspondant pas à la marque affichée sur la page.

Le composant classe les liens qu'il trouve et expose le type d'action et le chemin cible pour chacun, de sorte qu'un audit puisse lister chaque action de lancement et URI avec sa destination. Il s'agit de rapport, non d'exécution. L'auditeur lit l'action dans la structure et l'enregistre. Il ne la suit jamais.

Le contrôle de visualisation qui affiche les documents est l'endroit où le suivi d'une action se produirait, et sa posture par défaut est délibérément prudente. Le contrôle TPdfView possède un ensemble LinkOptions qui décide des types de liens activés automatiquement lors d'un clic. Sa valeur par défaut est [loAutoGoto, loAutoOpenURI], ce qui signifie que les sauts internes et les URL web peuvent s'ouvrir, mais loAutoLaunch est absent, donc les actions de lancement ne s'exécutent jamais automatiquement. Pour un flux d'audit, vous allez plus loin et videz complètement cet ensemble, afin que rien ne s'active automatiquement pendant que vous évaluez la sécurité du fichier.

// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];

// The shipped default already withholds launch:
//   default = [loAutoGoto, loAutoOpenURI]
//   loAutoLaunch is NOT in the default set, so external programs
//   are never started on a stray click out of the box.

Le raisonnement derrière le blocage des lancements par défaut est simple. Un saut interne est inoffensif et une URL est visible et annulable, mais démarrer un programme externe arbitraire à partir d'un clic est l'action la plus dangereuse qu'un lien PDF puisse demander ; elle est donc désactivée à moins d'une activation explicite. Un auditeur désactive même les comportements sûrs, car sa tâche consiste à observer, non à agir.

Le niveau de permission MDP de signature numérique

Les signatures modifient la donne. Une signature simple atteste des octets au moment de la signature. Une signature de certification, générée avec une règle de détection et de prévention des modifications du document (MDP), va plus loin : elle déclare ce qui peut légitimement changer après la certification, et un lecteur conforme avertit si des éléments hors de cette autorisation ont été touchés. Lire ce niveau de permission indique à un auditeur si un fichier est certifié et, si oui, à quel point il doit être verrouillé.

La permission MDP est un entier possédant trois valeurs définies. Un niveau 1 signifie qu'aucune modification n'est autorisée ; tout changement rompt la certification. Un niveau 2 permet le remplissage de formulaires et la signature, cas classique d'un contrat destiné à être rempli et signé mais non modifié. Un niveau 3 autorise en outre les annotations en plus du remplissage et de la signature. Connaître ce niveau permet à votre logique de réception de raisonner sur l'intention : un document certifié au niveau 1 qui comporte néanmoins des champs de formulaire ou des scripts est en contradiction, et cette anomalie mérite d'être signalée.

Le composant lit le nombre de signatures et présente chacune sous forme d'enregistrement dont le champ Permission contient cette valeur MDP, renseignée directement depuis l'appel sous-jacent FPDFSignatureObj_GetDocMDPPermission. Une permission égale à zéro signifie que la signature n'est pas une signature de certification (DocMDP), il n'y a donc pas de verrouillage au niveau du document à signaler.

var
  I: Integer;
  Sig: TPdfSignature;
begin
  if Pdf.SignatureCount = 0 then
    WriteLn('document is not signed')
  else
    for I := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[I];
      case Sig.Permission of
        1: WriteLn('certified: no changes allowed');
        2: WriteLn('certified: form fill and signing allowed');
        3: WriteLn('certified: form fill, signing and annotations allowed');
      else
        WriteLn('signed, but not a DocMDP certification');
      end;
    end;
end;

Le reste de la surface : fichiers joints et XFA

Deux éléments supplémentaires complètent l'inventaire. Les fichiers joints sont des documents entiers transportés au sein du PDF sous forme de pièces jointes, et ils constituent un vecteur de transmission classique, car un rapport d'apparence anodine peut embarquer un exécutable ou un second PDF malveillant dans son arborescence de pièces jointes. Le composant expose le nombre de pièces jointes et le nom de chacune, permettant à l'audit de lister le contenu embarqué sans rien extraire ni ouvrir.

La présence de XFA est l'autre indicateur. Un formulaire XFA remplace l'AcroForm statique par une architecture de formulaire basée sur XML qui apporte son propre modèle de rendu et de script, surface plus vaste et complexe qu'un simple formulaire. N'ayez pas besoin de traiter le XFA pour noter sa présence ; celle-ci signale simplement que le fichier comporte une couche interactive plus riche méritant un examen attentif. Le composant la signale par un simple booléen.

var
  I: Integer;
begin
  if Pdf.XFA then
    WriteLn('NOTE: document contains an XFA form layer');

  if Pdf.AttachmentCount > 0 then
  begin
    WriteLn('embedded files: ', Pdf.AttachmentCount);
    for I := 0 to Pdf.AttachmentCount - 1 do
      WriteLn('  - ', Pdf.AttachmentName[I]);
  end;
end;

Une routine en lecture seule qui rédige un rapport

Assemblez ces éléments et l'audit devient une procédure unique qui charge un document, énumère ses scripts et leurs corps, liste ses cibles de lancement et d'URI, signale le niveau MDP des signatures, note les pièces jointes et le XFA, et enregistre les résultats. Elle n'affiche rien, ce qui la rend peu coûteuse et l'empêche d'être trompée pour afficher un contenu de page hostile. La sortie est un enregistrement simple et lisible par l'homme sur lequel un réviseur ou une règle en aval peut s'appuyer.

La méthode efficace consiste à collecter chaque constat sous forme de ligne, à préfixer ceux présentant un risque réel pour les placer en tête de file d'attente de révision, et à conserver le tout à côté du fichier. Un document sans script, sans action de lancement, sans pièce jointe, sans XFA, et sans signature ou doté d'une certification cohérente passe sans encombre. Un document qui active plusieurs indicateurs à la fois est celui qu'une personne doit examiner avant toute ouverture ultérieure. L'audit ne prend pas la décision de confiance pour vous. Il veille à ce que cette décision soit éclairée plutôt qu'aveugle.

Une fois que le fichier a passé l'audit et que vous devez le consulter, faites-le sous restriction plutôt que dans un visualiseur par défaut. L'approche décrite dans notre guide de construction d'un aperçu PDF sécurisé dans Delphi montre comment empêcher la gestion automatique des liens et l'activité des contenus actifs lors d'une consultation contrôlée. Pour intégrer cette énumération dans une chaîne de traitement complète avec des outils de révision, reportez-vous à l'article sur l'espace de travail d'intégration et de révision PDF. Tous s'appuient sur la même base en lecture seule et sans rendu et sont fournis avec le composant PDFium pour Delphi et C++Builder, aux côtés des API de rendu, de texte, de formulaire et de signature traitées par ailleurs sur ce blog.