Műszaki cikk

PDF-képkodekek izolálása feldolgozó folyamatban HotPDF-fel

A HotPDF a három legkockázatosabb PDF-képszűrőt, a DCTDecode-ot, a JPXDecode-ot és a JBIG2Decode-ot, egy különálló, rövid életű feldolgozó folyamatban tudja dekódolni az alkalmazásod helyett. Ezt a CodecIsolationMode tulajdonság kapcsolja be, és a gyakorlati hatás az, hogy egy hibás JPEG 2000 kódfolyam, amely korábban összeomlasztotta volna a VCL-alkalmazásodat, most egy eldobható gyermekfolyamatot öl meg, miközben a gazdaalkalmazás jelent egy állapotkódot és folytatja a működést

Ez a különbség ott számít igazán, ahonnan a PDF-ek ténylegesen érkeznek: feltöltési űrlap, levelezési átjáró, szkennelő eszköz, partneri FTP-mappa. Ezeket a byte-okat nem te irányítod, és a képkodekek azok, ahol a történelmi károk nagy része keletkezett

Miért dönt le egyetlen hibás kép egy teljes alkalmazást?

Mert a képkodek az egyetlen olyan része egy PDF-olvasónak, amely egy összetett állapotgépet futtat támadó által vezérelt adatokon, szinte semmilyen strukturális ellenőrzésre nem támaszkodva. Mire a byte-ok elérik a JPEG 2000 vagy a JBIG2 dekódolót, a kereszthivatkozás-tábla már fel van dolgozva, az objektum fel van oldva, a szűrőlánc le van fejtve, és ami marad, az egy nyers kódfolyam, amely megmondja, hány csempe, hány komponens és hány bit/minta van. Egy rossz szám itt nem elemzési hiba. Ez egy hibás allokációs méret vagy egy tartományon kívüli index egy szűk dekódolási ciklusban

A keretkorlátok segítenek, és ezeknek már eleve a helyükön kellene lenniük. A HotPDF a DecodeBudgetBytes és a DocumentDecodeBudgetBytes tulajdonságokkal korlátozza a kibontást, valamint a DecodeFilterLimit és a DecodePipelineDepthLimit tulajdonságokkal a szűrőláncokat; az ezek mögötti megfontolást a beágyazott szűrők és PDF-bombák korlátozott dekódolása című cikk tárgyalja. Egy byte-keret azonban csak egy kérdésre válaszol: mennyi kimenet engedélyezett. Arra nem tud válaszolni, mi történik, ha a dekódoló még azelőtt hibázik, hogy bármilyen kimenetet előállítana. Egy hozzáférési hiba egy dekódolási cikluson belül nem olyan szabálysértés, amelyet vissza lehet utasítani; ez egy folyamatszintű esemény, és egy folyamatszintű esemény egyetlen megbízható elszigetelése egy másik folyamat

Mit izolál a HotPDF, és mit nem

A HotPDF pontosan három kodektípust izolál, amelyeket a HPDFCodecIsolation unit hckDCT, hckJPX és hckJBIG2 néven sorol fel. Minden más, a Flate, az LZW, a RunLength, az ASCII85, a CCITT, a folyamaton belül marad, mert ezek a dekódolók elég egyszerűek ahhoz, hogy kerettel korlátozhatók legyenek, és nem innen erednek az érdekes hibák

A szállítási réteg szándékosan szűk. A gazdaalkalmazás lefoglal egy korlátozott méretű megosztott memória leképezést, beír egy rögzített méretű THPDFCodecSharedHeader fejlécet a tömörített bemenettel és az esetleges JBIG2 globális szegmensekkel együtt, elindítja a worker folyamatot, és vár. A worker a dekódolt pixeleket visszaírja ugyanabba a leképezésbe, és beállít egy állapotszót. Nincs csővezeték-protokoll, amely kizökkenhetne a szinkronból, nincs szerializációs formátum, amelyet fuzzolni lehetne, és a fejléc egy mágikus értéket és egy verziószámot hordoz, így egy nem megfelelő worker bináris elutasításra kerül, ahelyett hogy félreértelmezésre kerülne

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Zárt hiba esetén: ezeket a kodekeket soha ne dekódold a folyamaton belül
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 vagy >= 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;

Ha a CodecWorkerExecutable értéket üresen hagyod, a HotPDF a saját futtatható állományod mellett keresi a workert, mint HotPDFCodecWorker.exe, a ParamStr(0) könyvtárában. Állítsd be explicit módon, ha a telepítésed máshova helyezi a workert; az érték a ExpandFileName függvényen keresztül kerül kibontásra, így egy relatív útvonal az aktuális könyvtárhoz, nem az alkalmazás könyvtárához viszonyítva oldódik fel, ami egy szolgáltatásnál ritkán az, amit szeretnél

Automatikus vagy kötelező: melyik hibát preferálod?

A THPDFCodecIsolationMode három értéke három különböző választ kódol egyetlen kérdésre: mi történjen, ha a worker egyáltalán nem tud futni. A cimDisabled teljesen kihagyja az izolációt, és a folyamaton belül dekódol, ez a 3.x előtti viselkedés. A cimAutomatic, az alapértelmezett, megpróbálja a workert, és csendben visszavált a folyamaton belüli dekódolásra, ha a worker futtatható állománya hiányzik vagy nem indul el, amit cwsUnavailable állapotként jelent. A cimRequired elutasítja ezt a tartalék megoldást: egy elérhetetlen worker a dekódolást kezeltként és sikertelenként jelöli meg, így egyetlen nem megbízható kódfolyam sem éri el a címtartományodat

A fenyegetettségi modell alapján válassz, ne kényelmi szempontból. Egy asztali megjelenítő, amely a felhasználó lemezén már meglévő dokumentumokat nyit meg, jól működik cimAutomatic módban, ahol egy hiányzó worker a klasszikus viselkedésre fokozódik le, ahelyett hogy megtörné a terméket. Egy internetről fájlokat feldolgozó beviteli szolgáltatásnak cimRequired módban kell futnia, mert egy telepítési hiba, amely csendben eldobja az izolációs réteget, pontosan az a fajta regresszió, amelyet senki sem vesz észre, amíg nem lesz belőle probléma. Figyeld meg az aszimmetriát: csak a cwsUnavailable vált ki tartalék megoldást. Egy worker, amely elindult, majd összeomlott, túllépte az időkorlátot vagy elért egy korlátot, mindkét módban dekódolási hibának számít, sosem csendes újrapróbálkozásnak a folyamaton belül

A THPDFCodecWorkerStatus eredményének értelmezése

A GetLastCodecWorkerInfo a legutóbbi izolált dekódolás eredményét adja vissza, és az állapot-felsorolás elég specifikus ahhoz, hogy valódi üzemeltetési döntéseket vezéreljen, nem csupán egy általános „a kép sikertelen” naplósort. Az értékek a következők: cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError és cwsOutputLimit

Kezeld ezeket három csoportként. Telepítési problémák a cwsUnavailable és a cwsLaunchFailed: valaki worker nélkül szállított, vagy egy vírusirtó blokkolja a folyamatlétrehozást. Dokumentumproblémák a cwsDecodeFailed és a cwsOutputLimit: a fájl hibás vagy nagyobb, mint amit a szabályzatod megenged, és az elutasítás a helyes válasz. Az érdekes csoport a cwsTimedOut és a cwsCrashed, mert ezek azok az események, amelyek korábban lefagyasztották vagy megölték volna a gazdafolyamatot. Amikor ez történik, a hozzá tartozó ProcessId, ExitCode és ElapsedMilliseconds mezők elegendők ahhoz, hogy összevesd egy Windows Error Reporting bejegyzéssel, és eldöntsd, hogy egy ügyfél fájlja kóros-e, vagy valaki téged vizsgál

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // nincs mit jelenteni
    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;

A korlátok, amelyek ténylegesen érvényesülnek

Három külön felső korlát vonatkozik minden izolált dekódolásra, és annak ismerete, hogy melyik lépett életbe, egy egész délutánnyi találgatást spórol meg. A CodecWorkerTimeoutMilliseconds alapértelmezett értéke 10 000, és az 1-től 600 000-ig terjedő tartományba validálódik; egy tartományon kívüli érték kivételt dob, nem csendben korlátozza magát. A CodecWorkerMemoryLimitBytes alapértelmezett értéke 536 870 912 byte, és vagy nullának kell lennie, ami korlátlant jelent, vagy legalább 67 108 864 byte-nak, mert egy kisebb korlát nem tudna egy reális dekódoló munkakészletet befogadni, és minden dokumentumnál elbukna. A memóriakorlátot egy Windows Job Object kényszeríti ki kill-on-close szemantikával, így a worker a joborral együtt hal meg akkor is, ha a gazdaalkalmazás hirtelen leáll

A harmadik korlát a kimeneti korlát, amely levezetett, nem konfigurált érték. A HotPDF a szükséges byte-okat a kért régióból vagy a várt képgeometriából számítja ki, szélesség szorozva magassággal szorozva hárommal 24 bites kimenet esetén, majd ezt az értéket lecsökkenti a DecodeBudgetBytes értékére, ha be van állítva egy keret. Egy dekódoló, amely hihető fejlécet jelent, majd megpróbál sokkal több pixelt kiadni, mint amennyit a geometria megenged, magát a leképezést ütközteti, és a gazdaalkalmazás cwsOutputLimit értéket lát. Ezért egészíti ki egymást az izolációs réteg és a dekódolási keret: a keret határozza meg, mekkora lehet egy kép, az izolációs határ pedig biztosítja, hogy egy hazugság erről a méretről ne válhasson tartományon kívüli írássá a folyamatodban

Hol illeszkedik ez egy megerősített befogadási útvonalba

A folyamatizoláció egy sokkal korábban kezdődő védelmi lánc legkülső rétege. A strukturális korlátok elutasítják a valószínűtlen dokumentumokat elemzéskor. A szűrőkeretek korlátozzák a kibontást. Az izoláció megfogja azt, ami mindkettőt túléli. Az olyan dokumentumoknál, amelyek elérik a képréteget, érdemes tudni, éppen melyik kodeket futtatod, mivel a JPXDecode kezelése és a JBIG2 szimbólumszótárak nagyon eltérő hibaprofillal rendelkeznek, és a JBIG2 esetében különösen igaz, hogy oldalak közötti globális szegmenseket hordoz, amelyeket egy naiv, képenkénti sandbox megtörne

A költség őszinte és érdemes kimondani: egy folyamat elindítása minden izolált képhez milliszekundumokat ad hozzá, és egy több száz szkennelt oldalt tartalmazó dokumentum ezt megérzi. Mérd ezt ahhoz, amit cserébe kapsz. Egy éjszaka felügyelet nélkül futó kötegelt konvertálónál az átviteli sebesség vesztesége láthatatlan, és az összeomlás-elszigetelés az egész lényeg. Egy interaktív megjelenítőnél, amely olyan dokumentumokat nyit meg, amelyekben a felhasználó már megbízik, a cimDisabled vagy a cimAutomatic az ésszerű alapértelmezés. A mód egy egyszerű tulajdonság, így semmi sem akadályoz meg abban, hogy futásidőben dokumentumosztályonként válassz

A HotPDF az izolációs réteget, a dekódolási kereteket és a strukturális elemzési korlátokat egyetlen natív VCL komponensként szállítja Delphihez és C++Builderhez, a worker futtatható állományon kívül semmilyen külső futtatókörnyezetet nem igényel telepítéshez. A teljes API-dokumentáció és egy próbaverzió elérhető a HotPDF Delphi PDF komponens oldalán