Tehnički članak

Izolujte PDF kodeke slika u worker procesima uz HotPDF

HotPDF može da dekodira tri najrizičnija PDF filtera za slike, DCTDecode, JPXDecode i JBIG2Decode, unutar odvojenog, kratkotrajnog worker procesa umesto unutar vaše aplikacije. Svojstvo koje ovo uključuje je CodecIsolationMode, a praktičan efekat je da neispravan JPEG 2000 kodni tok, koji bi ranije srušio vašu VCL aplikaciju, sada ubija potrošni dete-proces, dok host prijavljuje statusni kod i nastavlja dalje

Ta razlika je najvažnija upravo na mestima odakle PDF-ovi zaista stižu: formular za otpremanje, mejl gateway, uređaj za skeniranje, FTP nalog partnera. Vi ne kontrolišete te bajtove, a kodeci slika su mesto gde istorijski živi najveća šteta

Zašto jedna loša slika obori celu aplikaciju?

Zato što je kodek za slike upravo onaj deo PDF čitača koji izvršava složenu mašinu stanja nad podacima pod kontrolom napadača, gotovo bez ijedne strukturne provere na koju bi se mogao osloniti. Do trenutka kada bajtovi stignu do JPEG 2000 ili JBIG2 dekodera, tabela unakrsnih referenci je već parsirana, objekat je razrešen, lanac filtera je odmotan, a ostaje samo sirov kodni tok koji kaže koliko ima tajlova, koliko komponenti, koliko bitova po uzorku. Pogrešan broj tu nije greška parsiranja. To je loša veličina alokacije ili indeks van opsega unutar zategnute petlje dekodiranja

Budžetska ograničenja pomažu, i trebalo bi da ih već imate. HotPDF ograničava širenje pomoću DecodeBudgetBytes i DocumentDecodeBudgetBytes, a ograničava lance filtera pomoću DecodeFilterLimit i DecodePipelineDepthLimit; logika iza tih ograničenja pokrivena je u ograničenom dekodiranju za ugnežđene filtere i PDF bombe. Ali budžet bajtova odgovara samo na jedno pitanje, koliko je izlaza dozvoljeno. On ne može da odgovori šta se dešava kada dekoder zapadne u grešku pre nego što uopšte proizvede bilo kakav izlaz. Kršenje pristupa memoriji unutar petlje dekodiranja nije prekršaj politike koji možete odbiti; to je događaj na nivou procesa, a jedino pouzdano zadržavanje za događaj na nivou procesa je drugi proces

Šta HotPDF izoluje, a šta ne

HotPDF izoluje tačno tri vrste kodeka, nabrojane kao hckDCT, hckJPX i hckJBIG2 u jedinici HPDFCodecIsolation. Sve ostalo, Flate, LZW, RunLength, ASCII85, CCITT, ostaje u istom procesu, jer su ti dekoderi dovoljno jednostavni da se ograniče budžetima i nisu mesto odakle dolaze zanimljivi otkazi

Transport je namerno uzak. Host alocira jedno ograničeno mapiranje deljene memorije, upisuje fiksni THPDFCodecSharedHeader plus komprimovan ulaz i eventualne JBIG2 globalne segmente, pokreće worker i čeka. Worker upisuje dekodirane piksele nazad u isto mapiranje i postavlja statusnu reč. Ne postoji protokol nad cevi (pipe) koji bi mogao da se raskonči, ne postoji format serijalizacije za fazovanje (fuzzing), a zaglavlje nosi magičnu vrednost i verziju, tako da se neusaglašen binarni fajl worker-a odbija umesto da bude pogrešno pročitan

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail-closed: nikada ne dekoduj ove kodeke u istom procesu
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 ili >= 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;

Ostavite CodecWorkerExecutable praznim i HotPDF će pronaći worker pored vašeg sopstvenog izvršnog fajla, kao HotPDFCodecWorker.exe u direktorijumu iz ParamStr(0). Podesite ga eksplicitno kada vaš deployment stavlja worker negde drugde; vrednost se proširuje kroz ExpandFileName, tako da se relativna putanja razrešava u odnosu na trenutni direktorijum, a ne na direktorijum aplikacije, što retko kada želite kod servisa

Automatski ili obavezan: koji otkaz preferirate?

Tri vrednosti tipa THPDFCodecIsolationMode kodiraju tri različita odgovora na jedno pitanje, šta treba da se desi kada worker uopšte ne može da se pokrene. cimDisabled u potpunosti preskače izolaciju i dekodira u istom procesu, ponašanje iz vremena pre verzije 3.x. cimAutomatic, podrazumevana vrednost, pokušava sa worker-om i tiho se vraća na dekodiranje u istom procesu kada izvršni fajl worker-a nedostaje ili ne može da se pokrene, što se prijavljuje kao status cwsUnavailable. cimRequired odbija taj fallback: nedostupan worker označava dekodiranje kao obrađeno i neuspešno, tako da nijedan nepouzdan kodni tok nikada ne dopre do vašeg adresnog prostora

Birajte prema modelu pretnji, ne prema pogodnosti. Desktop pregledač koji otvara dokumente koje korisnik već ima na disku sasvim je u redu sa cimAutomatic, gde nedostajući worker degradira na klasično ponašanje umesto da pokvari proizvod. Servis za unos koji parsira fajlove sa interneta trebalo bi da radi sa cimRequired, jer greška u deployment-u koja tiho ukine sloj izolacije predstavlja upravo onu vrstu regresije koju niko ne primeti dok ne bude prekasno. Primetite asimetriju: samo cwsUnavailable pokreće fallback. Worker koji se pokrenuo, a zatim se srušio, istekao je ili je udario u ograničenje predstavlja otkaz dekodiranja u oba režima, nikada tihi ponovni pokušaj u istom procesu

Čitanje presude iz THPDFCodecWorkerStatus

GetLastCodecWorkerInfo vraća ishod poslednjeg izolovanog dekodiranja, a enumeracija statusa je dovoljno precizna da pokrene stvarne operativne odluke, a ne generičku log liniju „slika nije uspela”. Vrednosti su cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError i cwsOutputLimit

Tretirajte ih kao tri grupe. Problemi sa deployment-om su cwsUnavailable i cwsLaunchFailed: neko je isporučio bez worker-a, ili antivirusni proizvod blokira kreiranje procesa. Problemi sa dokumentom su cwsDecodeFailed i cwsOutputLimit: fajl je neispravan ili veći nego što vaša politika dozvoljava, a odbijanje je ispravan odgovor. Zanimljiva grupa su cwsTimedOut i cwsCrashed, jer su to događaji koji bi ranije zamrznuli ili ubili host proces. Kada se to dogodi, prateća polja ProcessId, ExitCode i ElapsedMilliseconds daju vam dovoljno da korelišete sa unosom Windows Error Reporting-a i odlučite da li je jedan fajl klijenta patološki ili vas neko sondira

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // nema šta da se prijavi
    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;

Ograničenja koja zaista deluju

Tri odvojena praga važe za svako izolovano dekodiranje, a znanje koji je od njih okinuo štedi popodne nagađanja. CodecWorkerTimeoutMilliseconds podrazumevano iznosi 10.000 i validira se u opsegu od 1 do 600.000; vrednost van tog opsega izaziva izuzetak umesto da se tiho ograniči (clamp). CodecWorkerMemoryLimitBytes podrazumevano iznosi 536.870.912 bajtova i mora biti ili nula, što znači bez ograničenja, ili najmanje 67.108.864 bajtova, jer manje ograničenje ne može da drži realističan radni skup dekodera i propustilo bi svaki dokument. Memorijsko ograničenje sprovodi Windows Job Object sa semantikom kill-on-close, tako da worker umire zajedno sa job objektom čak i ako je host naglo prekinut

Treći prag je ograničenje izlaza, i ono je izvedeno, a ne konfigurisano. HotPDF izračunava potreban broj bajtova iz tražene oblasti, ili iz očekivane geometrije slike, kao širina puta visina puta tri za 24-bitni izlaz, a zatim ograničava tu vrednost na DecodeBudgetBytes kada je budžet postavljen. Dekoder koji prijavi verodostojno zaglavlje, a zatim pokuša da emituje mnogo više piksela nego što geometrija dozvoljava, zaustavlja samo mapiranje, a host vidi cwsOutputLimit. Zbog toga se sloj izolacije i budžet dekodiranja dopunjuju: budžet definiše koliko velika slika sme biti, a granica izolacije obezbeđuje da laž o toj veličini ne može postati upis van granica u vašem procesu

Gde se ovo uklapa u ojačanu putanju prijema

Izolacija procesa je najspoljniji sloj lanca odbrane koji počinje mnogo ranije. Strukturna ograničenja odbijaju neverodostojne dokumente u trenutku parsiranja. Budžeti filtera ograničavaju širenje. Izolacija zadržava ono što preživi oba koraka. Za dokumente koji dopru do sloja slika, vredi znati koji kodek zapravo vežbate, pošto rukovanje JPXDecode-om i JBIG2 rečnici simbola imaju veoma različite profile otkaza, a JBIG2 posebno nosi globalne segmente koji se protežu kroz više stranica, što bi naivan sandbox po slici pokvario

Cena je iskrena i vredi je izreći: pokretanje procesa po izolovanoj slici dodaje milisekunde, a dokument sa stotinama skeniranih stranica će to osetiti. Uporedite to sa onim što dobijate zauzvrat. Na batch konvertoru koji radi bez nadzora preko noći, gubitak propusnosti je nevidljiv, a zadržavanje pada je čitava poenta. Na interaktivnom pregledaču koji otvara dokumente kojima korisnik već veruje, cimDisabled ili cimAutomatic je razumna podrazumevana vrednost. Režim je obično svojstvo, tako da vas ništa ne sprečava da birate po klasi dokumenta u toku izvršavanja

HotPDF isporučuje sloj izolacije, budžete dekodiranja i strukturna ograničenja parsera kao jednu izvornu VCL komponentu za Delphi i C++Builder, bez ikakvog spoljnog runtime-a za deployment osim samog izvršnog fajla worker-a. Potpuna API dokumentacija i probna verzija dostupne su na stranici HotPDF Delphi PDF komponente