Article technique

Rejouer des EMF et WMF non fiables sans risque dans HotXLS

Un classeur Excel peut porter des images EMF et WMF, et la façon conventionnelle d'en dessiner une est de remettre le flux d'octets au lecteur de métafichiers du système d'exploitation. C'est une décision qui mérite d'être regardée directement : un métafichier est un flux de commandes sérialisé pour une API graphique, et le rejouer signifie laisser un fichier arrivé par courriel piloter le pilote graphique. HotXLS prend l'autre voie. XLSDecodeVectorScene analyse le métafichier elle-même, valide l'en-tête, chaque taille d'enregistrement, le total d'enregistrements déclaré et le placement exact de l'enregistrement de fin de fichier, refuse carrément les enregistrements escape, et renvoie une TXLSVectorScene de commandes de dessin primitives que les backends Canvas et SVG rejouent par leur propre code. Aucune lecture par pilote n'est impliquée à aucun moment

HotXLS analyse des octets EMF et WMF non fiables de feuille de calcul avec XLSDecodeVectorScene en une liste de commandes TXLSVectorScene au lieu d'une lecture GDI de métafichier
HotXLS analyse lui-même le métafichier et renvoie des commandes primitives pour le rejeu Canvas et SVG ; la voie conventionnelle exécute le flux d'octets sur la pile graphique

Le marché est de la couverture contre du confinement. Une liste blanche de commandes orientée rectangles ne reproduira pas chaque métafichier qu'un designer peut créer, donc la scène signale combien d'enregistrements de dessin elle n'a pas pu représenter et l'appelant décide quoi en faire. Pour un processus serveur qui rend des documents qu'il n'a pas créés, ce marché est dans le bon sens

Pourquoi le rejeu de métafichier est-il un mauvais choix pour des entrées non fiables ?

Parce que le format n'est pas une image, c'est un programme. Un flux d'enregistrements EMF manipule une pile d'états de contexte de périphérique, alloue et sélectionne des objets depuis une table de handles, et peut porter des enregistrements escape dont la charge est passée à un pilote de périphérique. Le rejouer exerce des chemins de la pile graphique de la plateforme qui ont été écrits en supposant que le métafichier venait d'une application coopérante sur la même machine. Quand l'entrée est une pièce jointe de tableur, cette supposition a disparu, et aucun soin dans la bibliothèque tableur n'aide car la bibliothèque n'est pas le composant qui fait l'analyse

C'est le même raisonnement qui gouverne la couche conteneur. Un classeur est une archive ZIP, et HotXLS valide son répertoire central au lieu de faire confiance aux décalages déclarés, comme décrit dans l'article sur la validation de fin de répertoire central ZIP. Les charges de métafichier sont la couche suivante du même problème

Ce que le décodeur vérifie avant de dessiner quoi que ce soit

La validation est structurelle et elle a lieu d'emblée, car un analyseur qui commence à dessiner et valide en avançant a déjà agi sur des données qu'il n'a pas vérifiées. L'en-tête doit correspondre strictement plutôt que plausiblement. Chaque enregistrement doit déclarer une taille qui tient dans le tampon restant et est assez grande pour ses propres champs fixes. Le compte d'enregistrements que l'en-tête déclare doit correspondre aux enregistrements réellement présents. L'enregistrement de fin de fichier doit se situer exactement là où le flux se termine, pas simplement quelque part près, ce qui ferme l'astuce des déchets finaux qui cache une seconde charge derrière une image valide

Au-delà de la structure, le décodeur est en échec fermé sur la sémantique. Les enregistrements escape sont refusés, pas sautés. Un enregistrement changeant d'état que le décodeur ne modélise pas fait échouer le décodage plutôt que d'être ignoré, car ignorer un changement d'état signifie que chaque commande de dessin subséquente est exécutée dans un état que le fichier n'a pas demandé, et le résultat est une image fausse d'une manière que personne ne peut prédire. Les enregistrements de dessin hors du jeu de commandes pris en charge sont une autre affaire : ils sont comptés et sautés, car une forme manquante est un écart visible et rapportable plutôt qu'une corruption silencieuse

XLSDecodeVectorScene vérifie d'emblée l'en-tête, les tailles d'enregistrements, les totaux et le placement de fin de fichier, puis refuse les enregistrements escape et compte les enregistrements de dessin non pris en charge
Les vérifications structurelles tournent d'emblée et la sémantique en échec fermé refuse les enregistrements escape, tandis que les enregistrements de dessin non pris en charge sont seulement comptés et sautés

Les budgets font partie du contrat de format

Les formats vectoriels ont leur propre version de la bombe de décompression. Quelques kilo-octets d'enregistrements peuvent déclarer des polylignes à des centaines de millions de points, ou une image dont les dimensions déclarées se multiplient en téraoctets. Les bornes doivent donc être des constantes explicites plutôt que ce que la machine arrive à survivre

// Depuis lxVectorScene : le budget de décodage, énoncé plutôt qu'implicite
XL_VECTOR_MAX_RECORDS           = 1000000;
XL_VECTOR_MAX_HANDLES           = 4096;
XL_VECTOR_MAX_DC_DEPTH          = 32;
XL_VECTOR_MAX_COMMANDS          = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS      = 2000000;
XL_VECTOR_MAX_TEXT_CHARS        = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS  = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE        = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS      = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES       = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD             = 1000000000;

Deux d'entre eux méritent une note. Le plafond de profondeur de contexte de périphérique de 32 existe parce que les enregistrements SaveDC et RestoreDC s'imbriquent, et un flux déséquilibré peut empiler pour toujours ; 32 est généreux pour de vrais métafichiers et bon marché à imposer. Le plafond de coordonnées existe parce que les coordonnées alimentent une transformation, et une valeur proche des limites de la plage d'entiers produit un résultat transformé qui est soit infini soit bouclé, après quoi tout calcul de boîte englobante en aval est du charabia. Serrer les coordonnées à l'analyse est bien plus facile à raisonner que de défendre chaque consommateur de la géométrie

Constantes de budget de décodage dans lxVectorScene de HotXLS pour enregistrements, handles, profondeur de DC, commandes, points, texte, taille d'image et serrage des coordonnées
Chaque limite est une constante nommée imposée pendant l'analyse ; le plafond de profondeur de DC et le serrage des coordonnées méritent le plus d'attention

Utiliser la scène

Le décodeur rend un objet qui vous appartient, un compte de commandes, une taille nominale, et un compte d'enregistrements de dessin qu'il a choisi de ne pas représenter

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data porte la charge d'image brute prise depuis le classeur
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // Refusé : en-tête, bornes, totaux, placement de fin de fichier ou un budget
    LogReject('metafile rejected: ' + Error);
    Exit;
  end;
  try
    if Scene.SkippedDrawRecords > 0 then
      LogWarning(Format('%d drawing records outside the safe subset',
        [Scene.SkippedDrawRecords]));
    for I := 0 to Scene.Count - 1 do
      case Scene.Commands[I].Kind of
        xlsvcRectangle: DrawRect(Scene.Commands[I]);
        xlsvcEllipse:   DrawEllipse(Scene.Commands[I]);
        xlsvcPolyline,
        xlsvcPolygon,
        xlsvcBezier:    DrawPath(Scene.Commands[I]);
        xlsvcText:      DrawText(Scene.Commands[I]);
        xlsvcImage:     DrawImage(Scene.Commands[I]);
      end;
  finally
    Scene.Free;
  end;
end;

L'enregistrement de commande porte tout ce dont un backend a besoin et rien qui exige un périphérique : présence, couleur, largeur et style du stylo ; présence et couleur de la brosse ; la géométrie ; et pour le texte la chaîne, le nom de police, la taille, les styles et l'alignement. C'est ce qui rend la même scène utilisable à la fois par le moteur de rendu canvas à l'écran et par l'écrivain SVG, et c'est pourquoi la voie vectorielle ne diverge pas entre aperçu et export. Le rendu à l'écran du contenu de feuille de calcul en général est couvert dans l'article sur le rendu de grille VCL personnalisée

Refuser une image n'endommage pas le classeur

Une propriété importante de cette conception est qu'un décodage refusé n'affecte que le rendu. La charge d'origine reste dans le modèle, donc un classeur ouvert et resauvegardé emporte ses images métafichier octet pour octet, que le décodeur sûr ait pu les dessiner ou non. La voie raster bornée existante reste aussi disponible comme repli. En d'autres termes, l'analyseur strict verrouille ce qui est exécuté, pas ce qui est préservé, et c'est la distinction qui laisse un changement motivé par la sécurité sortir sans se transformer en changement de perte de données

La gestion des objets de dessin en général, y compris les parties du modèle d'objet qui survivent aux aller-retour intactes, est couverte dans l'article sur les graphiques, images et dessins

Où cela laisse un déploiement serveur

Si vous rendez des classeurs téléversés par les utilisateurs dans un service, la position pratique est maintenant défendable : les images métafichier sont analysées par du code que vous pouvez auditer, bornées par des constantes que vous pouvez lire, et jamais remises à un pilote graphique. La réserve honnête est la couverture. Les métafichiers complexes produits par des outils de dessin toucheront le compteur d'enregistrements sautés, et la réponse est de faire remonter le compteur plutôt que d'élargir la liste blanche en silence. Une image qui se rend partiellement et le dit est une conversation de support ; une image qui se rend faussement et ne dit rien est un rapport de bug d'un client

HotXLS gère XLS, XLSX, ODS et CSV nativement en Delphi et C++Builder sans Excel installé, et la même philosophie d'analyse bornée traverse ses couches conteneur, formule et dessin. Les détails de format et de sécurité sont listés sur la page produit du HotXLS Delphi spreadsheet component