Article technique

HotPDF : isoler les codecs image PDF en processus worker

HotPDF peut décoder les trois filtres d'image PDF les plus risqués, DCTDecode, JPXDecode et JBIG2Decode, dans un processus worker séparé et de courte durée au lieu de le faire dans votre application. La propriété qui active cela est CodecIsolationMode, et l'effet pratique est qu'un flux de code JPEG 2000 malformé qui aurait fait planter votre application VCL tue désormais un processus enfant jetable, pendant que l'hôte rapporte un code d'état et poursuit son exécution

Cette différence compte surtout aux endroits d'où proviennent réellement les PDF : un formulaire d'envoi, une passerelle de messagerie, un appareil de numérisation, un dépôt FTP partenaire. Vous ne contrôlez pas ces octets, et c'est dans les codecs d'image que résident historiquement les dégâts

Pourquoi une seule mauvaise image fait-elle tomber toute l'application ?

Parce qu'un codec d'image est la seule partie d'un lecteur PDF qui exécute une machine à états complexe sur des données contrôlées par un attaquant, avec presque aucun contrôle structurel restant sur lequel s'appuyer. Au moment où les octets atteignent le décodeur JPEG 2000 ou JBIG2, la table de références croisées a été analysée, l'objet a été résolu, la chaîne de filtres a été déroulée, et ce qui reste est un flux de code brut qui indique combien de tuiles, combien de composants, combien de bits par échantillon. Un mauvais nombre à cet endroit n'est pas une erreur d'analyse. C'est une taille d'allocation erronée ou un indice hors limites à l'intérieur d'une boucle de décodage serrée

Les limites de budget aident, et vous devriez déjà les avoir en place. HotPDF borne l'expansion avec DecodeBudgetBytes et DocumentDecodeBudgetBytes, et borne les chaînes de filtres avec DecodeFilterLimit et DecodePipelineDepthLimit ; le raisonnement derrière ces plafonds est couvert dans le décodage borné pour les filtres imbriqués et les bombes PDF. Mais un budget en octets ne répond qu'à une seule question, combien de sortie est autorisée. Il ne peut pas répondre à ce qui se passe quand le décodeur échoue avant même de produire la moindre sortie. Une violation d'accès à l'intérieur d'une boucle de décodage n'est pas une violation de politique que vous pouvez refuser ; c'est un événement au niveau du processus, et le seul confinement fiable pour un événement au niveau du processus est un autre processus

Ce que HotPDF isole, et ce qu'il n'isole pas

HotPDF isole exactement trois types de codecs, énumérés comme hckDCT, hckJPX et hckJBIG2 dans l'unité HPDFCodecIsolation. Tout le reste, Flate, LZW, RunLength, ASCII85, CCITT, reste dans le processus, car ces décodeurs sont assez simples pour être bornés par des budgets et ne sont pas l'origine des défaillances intéressantes

Le transport est délibérément étroit. L'hôte alloue une seule projection de mémoire partagée bornée, écrit un THPDFCodecSharedHeader fixe plus l'entrée compressée et les éventuels segments globaux JBIG2, lance le worker, et attend. Le worker réécrit les pixels décodés dans la même projection et positionne un mot d'état. Il n'y a pas de protocole de tube à désynchroniser, pas de format de sérialisation à fuzzer, et l'en-tête porte une valeur magique et une version, si bien qu'un binaire worker qui ne correspond pas est rejeté plutôt que mal interprété

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Échec fermé : ne jamais décoder ces codecs dans le processus
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 ou >= 64 MiB
    Pdf.DecodeBudgetBytes := 134217728;

    if Pdf.LoadFromFile('untrusted-upload.pdf') = 1 then
      if Pdf.GetLoadedImageCount > 0 then
      begin
        Bmp := Pdf.ExtractLoadedImage(0);
        try
          if Pdf.GetLastCodecWorkerInfo(Info) then
            LogCodecOutcome(Info);
        finally
          Bmp.Free;
        end;
      end;
  finally
    Pdf.Free;
  end;
end;

Laissez CodecWorkerExecutable vide et HotPDF résout le worker à côté de votre propre exécutable, sous le nom HotPDFCodecWorker.exe dans le répertoire de ParamStr(0). Définissez-le explicitement quand votre déploiement place le worker ailleurs ; la valeur est développée via ExpandFileName, si bien qu'un chemin relatif se résout par rapport au répertoire courant plutôt qu'au répertoire de l'application, ce qui est rarement ce que vous voulez sur un service

Automatique ou requis : quel échec préférez-vous ?

Les trois valeurs de THPDFCodecIsolationMode codent trois réponses différentes à une seule question, que doit-il se passer quand le worker ne peut pas s'exécuter du tout. cimDisabled ignore complètement l'isolation et décode dans le processus, le comportement d'avant la version 3.x. cimAutomatic, la valeur par défaut, tente le worker et revient silencieusement au décodage dans le processus quand l'exécutable worker est manquant ou refuse de se lancer, ce qui est signalé par le statut cwsUnavailable. cimRequired refuse ce repli : un worker indisponible marque le décodage comme traité et échoué, si bien qu'aucun flux de code non fiable n'atteint jamais votre espace d'adressage

Choisissez selon le modèle de menace, pas par commodité. Une visionneuse de bureau qui ouvre des documents que l'utilisateur possède déjà sur disque se satisfait de cimAutomatic, où un worker manquant dégrade vers le comportement classique au lieu de casser le produit. Un service d'ingestion qui analyse des fichiers venus d'internet doit exécuter cimRequired, car une erreur de déploiement qui supprime discrètement la couche d'isolation est exactement le genre de régression que personne ne remarque avant que cela compte. Notez l'asymétrie : seul cwsUnavailable déclenche un repli. Un worker qui s'est lancé puis a planté, expiré, ou atteint une limite est un échec de décodage dans les deux modes, jamais une nouvelle tentative silencieuse dans le processus

Lire le verdict depuis THPDFCodecWorkerStatus

GetLastCodecWorkerInfo renvoie le résultat du dernier décodage isolé, et l'énumération de statut est assez précise pour piloter de véritables décisions opérationnelles plutôt qu'une ligne de journal générique « image échouée ». Les valeurs sont cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError et cwsOutputLimit

Traitez-les comme trois groupes. Les problèmes de déploiement sont cwsUnavailable et cwsLaunchFailed : quelqu'un a livré sans le worker, ou un antivirus bloque la création de processus. Les problèmes de document sont cwsDecodeFailed et cwsOutputLimit : le fichier est malformé ou plus grand que ce que votre politique autorise, et le rejeter est la bonne réponse. Le groupe intéressant est cwsTimedOut et cwsCrashed, car ce sont les événements qui, auparavant, auraient bloqué ou tué le processus hôte. Quand cela se produit, les champs ProcessId, ExitCode et ElapsedMilliseconds qui les accompagnent vous donnent de quoi corréler avec une entrée de Windows Error Reporting et décider si le fichier d'un client est pathologique ou si quelqu'un vous sonde

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // rien à signaler
    cwsUnavailable, cwsLaunchFailed:
      Alert('Codec worker not deployed: ' + Info.ErrorMessage);
    cwsTimedOut, cwsCrashed:
      Quarantine(Format('pid %d exit %d after %d ms',
        [Info.ProcessId, Info.ExitCode, Info.ElapsedMilliseconds]));
  else
    RejectDocument(Info.ErrorMessage);
  end;
end;

Les limites qui s'appliquent réellement

Trois plafonds distincts s'appliquent à chaque décodage isolé, et savoir lequel s'est déclenché vous épargne un après-midi de suppositions. CodecWorkerTimeoutMilliseconds vaut 10 000 par défaut et est validé dans la plage 1 à 600 000 ; une valeur hors de cette plage lève une exception plutôt que d'être silencieusement écrêtée. CodecWorkerMemoryLimitBytes vaut 536 870 912 octets par défaut et doit être soit zéro, ce qui signifie aucune limite, soit au moins 67 108 864 octets, car un plafond plus petit ne peut pas contenir un ensemble de travail réaliste pour un décodeur et ferait échouer chaque document. Le plafond mémoire est appliqué par un Job Object Windows avec une sémantique kill-on-close, si bien que le worker meurt avec le job même si l'hôte est terminé brutalement

Le troisième plafond est la limite de sortie, et il est dérivé plutôt que configuré. HotPDF calcule les octets requis à partir de la région demandée, ou de la géométrie d'image attendue, comme largeur fois hauteur fois trois pour une sortie 24 bits, puis écrête cette valeur jusqu'à DecodeBudgetBytes quand un budget est défini. Un décodeur qui rapporte un en-tête plausible puis tente d'émettre bien plus de pixels que ce que la géométrie autorise est arrêté par la projection elle-même, et l'hôte voit cwsOutputLimit. C'est pourquoi la couche d'isolation et le budget de décodage sont complémentaires : le budget définit la taille maximale autorisée pour une image, et la limite d'isolation garantit qu'un mensonge sur cette taille ne peut pas se transformer en écriture hors limites dans votre processus

Où cela s'inscrit dans un chemin d'entrée durci

L'isolation de processus est la couche la plus externe d'une chaîne de défense qui commence bien plus tôt. Les limites structurelles rejettent les documents invraisemblables au moment de l'analyse. Les budgets de filtres bornent l'expansion. L'isolation contient ce qui survit aux deux. Pour les documents qui atteignent la couche image, il vaut la peine de savoir quel codec vous sollicitez réellement, car la gestion de JPXDecode et les dictionnaires de symboles JBIG2 ont des profils de défaillance très différents, et JBIG2 en particulier porte des segments globaux inter-pages qu'un bac à sable naïf par image casserait

Le coût est honnête et mérite d'être énoncé : lancer un processus par image isolée ajoute des millisecondes, et un document avec des centaines de pages numérisées le ressentira. Mesurez-le face à ce qu'il vous apporte. Sur un convertisseur par lots qui tourne sans surveillance la nuit, la perte de débit est invisible et le confinement des plantages est tout l'intérêt. Sur une visionneuse interactive qui ouvre des documents auxquels l'utilisateur fait déjà confiance, cimDisabled ou cimAutomatic est le choix par défaut raisonnable. Le mode est une simple propriété, rien ne vous empêche donc de choisir par classe de document à l'exécution

HotPDF livre la couche d'isolation, les budgets de décodage et les limites structurelles de l'analyseur au sein d'un seul composant VCL natif pour Delphi et C++Builder, sans aucun runtime externe à déployer au-delà de l'exécutable worker lui-même. La documentation API complète et une version d'essai sont disponibles sur la page HotPDF Delphi PDF component