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