Articol tehnic

Izolare codecuri imagine PDF în procese worker cu HotPDF

HotPDF poate decodifica cele mai riscante trei filtre de imagine PDF, DCTDecode, JPXDecode și JBIG2Decode, într-un proces worker separat și de scurtă durată, în loc de în interiorul aplicației tale. Proprietatea care activează acest lucru este CodecIsolationMode, iar efectul practic este că un codestream JPEG 2000 malformat care înainte îți bloca aplicația VCL acum omoară un proces copil de unică folosință, în timp ce gazda raportează un cod de stare și continuă

Această diferență contează cel mai mult acolo de unde ajung efectiv fișierele PDF: un formular de încărcare, o poartă de mail, un aparat de scanare, un depozit FTP al unui partener. Nu controlezi acei bytes, iar codecurile de imagine sunt locul unde s-au produs istoric cele mai multe daune

De ce o singură imagine defectă poate doborî întreaga aplicație?

Pentru că un codec de imagine este singura parte a unui cititor PDF care rulează o mașină de stare complexă peste date controlate de atacator, cu aproape niciun control structural rămas la care să se poată recurge. Până când byte-ii ajung la decodorul JPEG 2000 sau JBIG2, tabela cross-reference a fost analizată, obiectul a fost rezolvat, lanțul de filtre a fost desfășurat, iar ce rămâne este un codestream brut care spune câte tile-uri, câte componente, câți biți pe eșantion. Un număr greșit acolo nu este o eroare de parsare. Este o dimensiune de alocare greșită sau un index în afara intervalului, în interiorul unei bucle de decodare strânse

Limitele de buget ajută, și ar trebui să le ai deja. HotPDF limitează expansiunea cu DecodeBudgetBytes și DocumentDecodeBudgetBytes, și limitează lanțurile de filtre cu DecodeFilterLimit și DecodePipelineDepthLimit; raționamentul din spatele acestor plafoane este acoperit în decodarea limitată pentru filtre imbricate și PDF bombs. Dar un buget de bytes răspunde la o singură întrebare, cât output este permis. Nu poate răspunde la ce se întâmplă când decodorul eșuează înainte să producă vreun output. O access violation în interiorul unei bucle de decodare nu este o încălcare de politică pe care o poți respinge; este un eveniment la nivel de proces, iar singura izolare fiabilă pentru un eveniment la nivel de proces este un alt proces

Ce izolează HotPDF, și ce nu izolează

HotPDF izolează exact trei tipuri de codec, enumerate ca hckDCT, hckJPX și hckJBIG2 în unitatea HPDFCodecIsolation. Tot restul, Flate, LZW, RunLength, ASCII85, CCITT, rămâne în proces, pentru că acele decodoare sunt suficient de simple pentru a fi limitate cu bugete și nu sunt locul de unde vin eșecurile interesante

Transportul este în mod deliberat îngust. Gazda alocă o mapare de memorie partajată cu dimensiune limitată, scrie un THPDFCodecSharedHeader fix plus intrarea comprimată și eventualele segmente globale JBIG2, lansează worker-ul și așteaptă. Worker-ul scrie pixelii decodați înapoi în aceeași mapare și setează un cuvânt de stare. Nu există un protocol pipe care să se poată desincroniza, niciun format de serializare de testat cu fuzzing, iar header-ul poartă o valoare magică și o versiune, astfel încât un binar worker nepotrivit este respins, nu citit greșit

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: nu decodifica niciodată aceste codecuri în proces
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 sau >= 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;

Lasă CodecWorkerExecutable gol și HotPDF rezolvă worker-ul lângă propriul tău executabil, ca HotPDFCodecWorker.exe în directorul lui ParamStr(0). Setează-l explicit atunci când desfășurarea ta pune worker-ul altundeva; valoarea este expandată prin ExpandFileName, astfel încât o cale relativă se rezolvă față de directorul curent, nu față de directorul aplicației, ceea ce rareori este ce îți dorești pe un serviciu

Automat sau obligatoriu: ce tip de eșec preferi?

Cele trei valori ale THPDFCodecIsolationMode codifică trei răspunsuri diferite la o singură întrebare, ce ar trebui să se întâmple când worker-ul nu poate rula deloc. cimDisabled sare complet peste izolare și decodifică în proces, comportamentul dinainte de versiunea 3.x. cimAutomatic, valoarea implicită, încearcă worker-ul și revine silențios la decodarea în proces atunci când executabilul worker lipsește sau nu pornește, situație raportată ca statusul cwsUnavailable. cimRequired refuză acea revenire: un worker indisponibil marchează decodarea ca gestionată și eșuată, astfel încât niciun codestream nesigur nu ajunge vreodată în spațiul tău de adrese

Alege în funcție de modelul de amenințare, nu de conveniență. Un viewer desktop care deschide documente pe care utilizatorul le are deja pe disc este perfect pe cimAutomatic, unde un worker lipsă degradează la comportamentul clasic în loc să strice produsul. Un serviciu de ingestie care analizează fișiere de pe internet ar trebui să ruleze cu cimRequired, pentru că o greșeală de desfășurare care elimină silențios stratul de izolare este exact tipul de regresie pe care nimeni nu o observă până nu contează. Observă asimetria: doar cwsUnavailable declanșează revenirea. Un worker care a pornit și apoi a crăpat, a expirat timpul sau a atins o limită este un eșec de decodare în ambele moduri, niciodată o reîncercare silențioasă în proces

Interpretarea verdictului din THPDFCodecWorkerStatus

GetLastCodecWorkerInfo returnează rezultatul celei mai recente decodări izolate, iar enumerarea de stare este suficient de specifică pentru a susține decizii operaționale reale, nu doar o linie de log generică de tipul „imaginea a eșuat”. Valorile sunt cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError și cwsOutputLimit

Tratează-le ca trei grupuri. Problemele de desfășurare sunt cwsUnavailable și cwsLaunchFailed: cineva a livrat fără worker, sau un produs antivirus blochează crearea de procese. Problemele de document sunt cwsDecodeFailed și cwsOutputLimit: fișierul este malformat sau mai mare decât permite politica ta, iar respingerea lui este răspunsul corect. Grupul interesant este cwsTimedOut și cwsCrashed, pentru că acestea sunt evenimentele care înainte ar fi blocat sau ar fi omorât procesul gazdă. Când se întâmplă asta, câmpurile însoțitoare ProcessId, ExitCode și ElapsedMilliseconds îți oferă suficient pentru a corela cu o înregistrare Windows Error Reporting și a decide dacă un fișier al unui client este patologic sau cineva te testează

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

Limitele care chiar contează

Trei plafoane separate se aplică fiecărei decodări izolate, iar știind care dintre ele s-a declanșat îți economisește o după-amiază de ghicit. CodecWorkerTimeoutMilliseconds are valoarea implicită 10.000 și este validat în intervalul 1 până la 600.000; o valoare în afara acestuia ridică o excepție, nu limitează silențios. CodecWorkerMemoryLimitBytes are valoarea implicită 536.870.912 bytes și trebuie să fie fie zero, adică fără limită, fie cel puțin 67.108.864 bytes, pentru că un plafon mai mic nu poate susține un set de lucru realist al decodorului și ar eșua pentru fiecare document. Plafonul de memorie este impus printr-un Windows Job Object cu semantică kill-on-close, astfel încât worker-ul moare odată cu job-ul chiar dacă gazda este terminată brusc

Al treilea plafon este limita de output, și este derivat, nu configurat. HotPDF calculează bytes necesari din regiunea cerută, sau din geometria de imagine așteptată, ca lățime înmulțită cu înălțime înmulțită cu trei pentru output pe 24 de biți, apoi limitează acea valoare la DecodeBudgetBytes atunci când este setat un buget. Un decodor care raportează un header plauzibil și apoi încearcă să emită mult mai mulți pixeli decât permite geometria este oprit chiar de mapare, iar gazda vede cwsOutputLimit. De aceea stratul de izolare și bugetul de decodare se completează: bugetul definește cât de mare are voie să fie o imagine, iar granița de izolare se asigură că o minciună despre acea dimensiune nu poate deveni o scriere în afara limitelor în procesul tău

Unde se încadrează asta într-un flux de ingestie securizat

Izolarea proceselor este stratul cel mai exterior al unui lanț de apărare care începe mult mai devreme. Limitele structurale resping documentele implauzibile la momentul parsării. Bugetele de filtre limitează expansiunea. Izolarea conține ce supraviețuiește ambelor. Pentru documentele care ajung la stratul de imagine, merită să știi ce codec exersezi efectiv, întrucât gestionarea JPXDecode și dicționarele de simboluri JBIG2 au profiluri de eșec foarte diferite, iar JBIG2 în special poartă segmente globale între pagini pe care un sandbox naiv per-imagine le-ar strica

Costul este onest și merită menționat: lansarea unui proces pentru fiecare imagine izolată adaugă milisecunde, iar un document cu sute de pagini scanate va simți asta. Măsoară-l în raport cu ce îți oferă. Pe un convertor batch care rulează nesupravegheat peste noapte, pierderea de throughput este invizibilă, iar izolarea la crash este tot rostul. Pe un viewer interactiv care deschide documente în care utilizatorul are deja încredere, cimDisabled sau cimAutomatic este valoarea implicită rezonabilă. Modul este o proprietate simplă, așa că nimic nu te oprește să alegi per clasă de document la runtime

HotPDF livrează stratul de izolare, bugetele de decodare și limitele structurale ale parserului ca o singură componentă VCL nativă pentru Delphi și C++Builder, fără niciun runtime extern de desfășurat în afară de executabilul worker însuși. Documentația API completă și o versiune de test sunt disponibile pe pagina componentei HotPDF Delphi PDF