HotPDF umí dekódovat tři nejrizikovější filtry obrázků PDF – DCTDecode, JPXDecode a JBIG2Decode – uvnitř samostatného krátkodobého worker procesu místo přímo ve vaší aplikaci. Vlastnost, která to zapíná, je CodecIsolationMode, a praktický efekt je ten, že poškozený tok JPEG 2000, který by dřív spadl vaši aplikaci VCL, teď zabije jednorázový dětský proces, zatímco hostitel nahlásí stavový kód a pokračuje dál
Tento rozdíl má největší význam právě tam, odkud PDF ve skutečnosti přicházejí: nahrávací formulář, poštovní brána, skenovací zařízení, FTP úložiště partnera. Tyto bajty nemáte pod kontrolou a kodeky obrázků jsou přesně místo, kde historicky vznikaly škody
Proč jeden špatný obrázek spadne celou aplikaci?
Protože kodek obrázků je ta jediná část čtečky PDF, která spouští složitý stavový automat nad daty pod kontrolou útočníka, a to už téměř bez strukturálních kontrol, o které by se dalo opřít. V okamžiku, kdy bajty dorazí k dekodéru JPEG 2000 nebo JBIG2, je už tabulka křížových odkazů rozebraná, objekt je vyřešený, řetězec filtrů je rozbalený, a co zbývá, je surový tok, který říká, kolik je dlaždic, kolik komponent, kolik bitů na vzorek. Špatné číslo na tomto místě není chyba parsování. Je to špatná velikost alokace nebo index mimo rozsah uvnitř těsné dekódovací smyčky
Rozpočtové limity pomáhají a už byste je měli mít nastavené. HotPDF omezuje expanzi pomocí DecodeBudgetBytes a DocumentDecodeBudgetBytes a omezuje řetězce filtrů pomocí DecodeFilterLimit a DecodePipelineDepthLimit; zdůvodnění těchto limitů popisuje článek o ohraničeném dekódování vnořených filtrů a PDF bomb. Bajtový rozpočet ale odpovídá jen na jednu otázku – kolik výstupu je povoleno. Neumí odpovědět na to, co se stane, když dekodér selže dřív, než vůbec nějaký výstup vyprodukuje. Přístup mimo paměť uvnitř dekódovací smyčky není porušení pravidla, které můžete odmítnout; je to událost na úrovni procesu, a jediné spolehlivé zadržení pro událost na úrovni procesu je jiný proces
Co HotPDF izoluje a co ne
HotPDF izoluje přesně tři druhy kodeků, vyjmenované jako hckDCT, hckJPX a hckJBIG2 v jednotce HPDFCodecIsolation. Všechno ostatní – Flate, LZW, RunLength, ASCII85, CCITT – zůstává v procesu, protože tyto dekodéry jsou dost jednoduché na to, aby se daly omezit rozpočtem, a nejsou to místa, odkud pocházejí zajímavá selhání
Přenosová vrstva je záměrně úzká. Hostitel alokuje jedno ohraničené mapování sdílené paměti, zapíše pevnou THPDFCodecSharedHeader plus komprimovaný vstup a případné globální segmenty JBIG2, spustí worker a čeká. Worker zapíše dekódované pixely zpět do stejného mapování a nastaví stavové slovo. Neexistuje žádný protokol přes pipe, který by se mohl rozejít, žádný serializační formát, který by šel fuzzovat, a hlavička nese magickou hodnotu a verzi, takže neshodující se binárka workeru se odmítne, místo aby se špatně přečetla
uses
HPDFDoc, HPDFCodecIsolation;
var
Pdf: THotPDF;
Info: THPDFCodecWorkerInfo;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
// selhat uzavřeně: tyto kodeky nikdy nedekódovat v procesu
Pdf.CodecIsolationMode := cimRequired;
Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
Pdf.CodecWorkerTimeoutMilliseconds := 5000; // 1..600000
Pdf.CodecWorkerMemoryLimitBytes := 268435456; // 0 nebo >= 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;
Necháte-li CodecWorkerExecutable prázdné, HotPDF vyhledá worker vedle vaší vlastní spustitelné aplikace, jako HotPDFCodecWorker.exe v adresáři podle ParamStr(0). Nastavte ho explicitně, pokud vaše nasazení umístí worker jinam; hodnota se rozšiřuje pomocí ExpandFileName, takže relativní cesta se řeší vůči aktuálnímu adresáři, ne vůči adresáři aplikace, což u služby málokdy chcete
Automaticky, nebo povinně: jaké selhání preferujete?
Tři hodnoty THPDFCodecIsolationMode kódují tři různé odpovědi na jednu otázku – co se má stát, když worker vůbec nejde spustit. cimDisabled izolaci úplně přeskočí a dekóduje v procesu, což je chování z dob před verzí 3.x. cimAutomatic, výchozí hodnota, worker vyzkouší a tiše se vrátí k dekódování v procesu, když spustitelný soubor workeru chybí nebo se nespustí, což se hlásí jako stav cwsUnavailable. cimRequired tento návrat odmítá: nedostupný worker označí dekódování jako zpracované a selhané, takže žádný nedůvěryhodný tok nikdy nedosáhne vašeho adresního prostoru
Volte podle modelu hrozeb, ne podle pohodlnosti. Desktopová prohlížečka, která otevírá dokumenty, jež má uživatel už na disku, si vystačí s cimAutomatic, kde chybějící worker degraduje na klasické chování místo toho, aby rozbil produkt. Vstupní služba zpracovávající soubory z internetu by měla běžet s cimRequired, protože chyba při nasazení, která tiše vypustí izolační vrstvu, je přesně ten typ regrese, kterou nikdo nezpozoruje, dokud na tom nezáleží. Všimněte si asymetrie: návrat zpět spustí jen cwsUnavailable. Worker, který se spustil a pak spadl, vypršel časový limit nebo narazil na omezení, je selhání dekódování v obou režimech, nikdy tiché opakování v procesu
Čtení verdiktu z THPDFCodecWorkerStatus
GetLastCodecWorkerInfo vrací výsledek posledního izolovaného dekódování a výčet stavů je dostatečně konkrétní na to, aby řídil skutečná provozní rozhodnutí místo obecného řádku v logu ve stylu „obrázek selhal". Hodnoty jsou cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError a cwsOutputLimit
Berte je jako tři skupiny. Problémy s nasazením jsou cwsUnavailable a cwsLaunchFailed: někdo nasadil bez workeru, nebo antivirový produkt blokuje vytváření procesů. Problémy s dokumentem jsou cwsDecodeFailed a cwsOutputLimit: soubor je poškozený nebo větší, než dovoluje vaše politika, a jeho odmítnutí je správná odpověď. Zajímavá skupina je cwsTimedOut a cwsCrashed, protože to jsou přesně ty události, které by dřív hostitelský proces zatuhly nebo zabily. Když k tomu dojde, přidružená pole ProcessId, ExitCode a ElapsedMilliseconds vám dají dost informací na to, abyste to spojili se záznamem Windows Error Reporting a rozhodli, jestli je jeden zákaznický soubor patologický, nebo vás někdo zkouší
procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
case Info.Status of
cwsSucceeded:
; // nic k hlášení
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;
Limity, které se skutečně uplatní
Na každé izolované dekódování se vztahují tři samostatné stropy, a vědět, který z nich zafungoval, ušetří celé odpoledne hádání. CodecWorkerTimeoutMilliseconds má výchozí hodnotu 10 000 a validuje se do rozsahu 1 až 600 000; hodnota mimo tento rozsah vyvolá výjimku, místo aby se tiše ořízla. CodecWorkerMemoryLimitBytes má výchozí hodnotu 536 870 912 bajtů a musí být buď nula, což znamená bez limitu, nebo alespoň 67 108 864 bajtů, protože menší strop by neudržel realistickou pracovní sadu dekodéru a selhal by u každého dokumentu. Limit paměti se vynucuje pomocí Windows Job Object se sémantikou kill-on-close, takže worker zemře spolu s job objektem, i kdyby byl hostitel náhle ukončen
Třetí strop je limit výstupu, a ten se odvozuje, ne konfiguruje. HotPDF spočítá potřebný počet bajtů z požadované oblasti, nebo z očekávané geometrie obrázku jako šířka krát výška krát tři pro 24bitový výstup, a pak tuto hodnotu ořízne dolů na DecodeBudgetBytes, je-li rozpočet nastaven. Dekodér, který nahlásí věrohodnou hlavičku a pak se pokusí vydat mnohem víc pixelů, než geometrie dovoluje, zastaví samotné mapování a hostitel uvidí cwsOutputLimit. Proto se izolační vrstva a rozpočet na dekódování navzájem doplňují: rozpočet definuje, jak velký smí obrázek být, a izolační hranice zajišťuje, že lež o této velikosti se nemůže proměnit v zápis mimo hranice ve vašem procesu
Kam to zapadá do zabezpečené vstupní cesty
Izolace procesů je nejvzdálenější vrstva obranného řetězce, který začíná mnohem dřív. Strukturální limity odmítají nevěrohodné dokumenty už při parsování. Rozpočty filtrů omezují expanzi. Izolace zadržuje to, co přežije obojí. U dokumentů, které se dostanou až k obrazové vrstvě, se vyplatí vědět, jaký kodek vlastně používáte, protože zpracování JPXDecode a symbolové slovníky JBIG2 mají velmi odlišné profily selhání, a JBIG2 zvlášť nese mezistránkové globální segmenty, které by naivní sandbox po jednom obrázku rozbil
Cena je poctivá a stojí za to ji vyslovit: spouštění procesu na každý izolovaný obrázek přidává milisekundy a dokument se stovkami skenovaných stránek to pozná. Poměřte to s tím, co za to dostanete. U dávkového konvertoru, který běží bez dozoru přes noc, je ztráta propustnosti neviditelná a zadržení pádu je celý smysl. U interaktivní prohlížečky, která otevírá dokumenty, jimž už uživatel důvěřuje, je cimDisabled nebo cimAutomatic rozumnou výchozí hodnotou. Režim je obyčejná vlastnost, takže nic vám nebrání volit ho podle třídy dokumentu za běhu
HotPDF dodává izolační vrstvu, rozpočty na dekódování i strukturální limity parseru jako jednu nativní komponentu VCL pro Delphi a C++Builder, bez nutnosti nasazovat jakýkoli externí runtime kromě samotného spustitelného souboru workeru. Úplná dokumentace API i zkušební verze jsou dostupné na stránce HotPDF Delphi PDF component