Cjevovod za prihvat dokumenata prihvaća datoteke koje su napisali neznanci. Računi, skenovi, privitci s web obrasca: svaki od njih tvrdi da je PDF i nosi stotine brojeva prema kojima bi vaš parser trebao postupati. Duljine tokova, dimenzije slika, pomaci u bajtovima, reference na objekte — svaki od njih odabrala je osoba koja je izradila datoteku, a prekinuti prijenos ili namjerno loše oblikovan dokument će s vremenom jedan od tih brojeva staviti na mjesto gdje će nanijeti štetu. Razlika između parsera koji preživi takvu datoteku i onoga koji se sruši, ili nastavi raditi s oštećenom memorijom, predstavlja mali skup navika koje ne ovise o nekoj određenoj PDF biblioteci
Te navike dijele jednu premisu: vrijednost očitana iz datoteke jest tvrdnja, a ne mjerenje. Postaje upotrebljiva tek nakon što se provjeri u odnosu na nešto što je sam parser izmjerio — stvarnu veličinu datoteke, stvarni broj bajtova koje je dekoder proizveo, stvarnu dubinu rekurzije. Ono što slijedi je ta premisa primijenjena na mjesta gdje se parseri dokumenata zapravo lome
Deklarirana duljina je tvrdnja, a ne mjerenje
Najjednostavnije nepodudaranje jest duljina toka. Objekt PDF toka deklarira broj bajtova u ključu /Length, dok stvarni podaci leže između ključnih riječi stream i endstream. Ništa ih ne prisiljava da se slažu. Skraćena datoteka sadrži manje stvarnih bajtova od deklariranog broja; datoteka iz pokvarenog generatora može deklarirati duljinu koja seže izvan kraja datoteke ili zadire u susjedni objekt. Alocirajte prema deklariranoj vrijednosti i kopirajte dok ne naiđete na endstream pa ćete preplaviti međuspremnik; čitajte točno deklarirani broj bez provjere dostupnosti i ispast ćete s kraja datoteke. Neka deklarirana vrijednost upravlja alokacijom tek nakon što je ograničite prema izmjerenoj udaljenosti do kraja podataka, a neslaganje tretirajte kao točku odluke — popravite traženjem endstream ili odbacite tok — a nikada kao nešto u što ćete tiho povjerovati
Parametri slike koji opisuju veći raster od onoga što ste alocirali
Tokovi slika podižu uloge jer dva neovisna skupa brojeva opisuju iste piksele. Rječnik slika nosi /Width i /Height, a međuspremnici rastera se obično dimenzioniraju prema njima. Filtar dekodiranja nosi vlastitu geometriju: CCITTFaxDecode uzima /Columns, /Rows i /K iz svog DecodeParms, pri čemu /K odabire shemu Group 3 ili Group 4, a dekoder emitira (Columns + 7) div 8 bajtova po liniji skeniranja. Datoteka koja deklarira /Width 100, ali filtru preda /Columns 1728 — što je zadano — uzrokuje da dekoder proizvede više od šesnaest puta više bajtova po retku od onoga što međuspremnik očekuje, a taj prelijevani dio slijeće red po red skeniranja na ono što god se nalazilo nakon alokacije. Kada nedostaje /Rows, dekoder radi dok podaci ne kažu da stane, pa stoga ograničite i broj redaka. DCTDecode ima isti šav: JPEG podaci nose vlastitu širinu i visinu u svom SOF markeru, i ništa ih ne obvezuje da se podudaraju s rječnikom
Obrambeno pravilo je mehaničko: izračunajte očekivanu veličinu rastera iz potvrđenih parametara dekodiranja — filtarovih vlastitih /Columns i /Rows za CCITT, SOF dimenzija za DCT — provjerite u odnosu na vaša ograničenja, alocirajte na temelju toga te tijekom dekodiranja potvrdite da izlaz nikada ne premašuje alokaciju. Kada se rječnik i filtar ne slažu u vezi s geometrijom, uskladite ih ili odbacite sliku. Ono što parser nikada ne smije učiniti je dimenzionirati međuspremnik na temelju jednog skupa brojeva, a dopustiti da dekoder radi prema drugom
Delphi aritmetika i zamke prilikom alokacije
Tri ponašanja Delphija potkopavaju čak i parser koji namjerava provoditi provjeru valjanosti. Prvo je 32-bitno množenje: Delphi evaluira umnožak dvaju Integer operanada na 32 bita, bez obzira na širinu odredišta, stoga Width * Height * BytesPerPixel može prekoračiti čak i kada svaki faktor samostalno prođe vlastitu provjeru ispravnosti. Skeniranje 30000 s 30000 pri tri bajta po pikselu iznosi 2.7 milijardi bajtova, što u predznačenoj 32-bitnoj aritmetici prekoračuje u minus; nešto drugačiji faktori prekoračuju na malu pozitivnu duljinu koja alocira preveliki i poddimenzionirani međuspremnik. Prisilite cijeli izraz na široko izvršavanje tako da castate prvi operand — Size := Int64(Width) * Height * BytesPerPixel — a zatim ga usporedite s eksplicitnim gornjim ograničenjem prije nego što išta dođe do SetLength
Drugo je provjera raspona. Delphijeva zadana konfiguracija izdanja isporučuje je isključenu, stoga se indeks izvan raspona izračunat iz podataka datoteke ne diže — već čita ili zapisuje u memoriju susjednu nizu. Uključite je natrag pomoću {$R+} (i {$Q+} za aritmetički preljev) na vrhu svake jedinice koja indeksira pomoću vrijednosti dobivenih iz datoteke. Trošak je nemjerljiv u usporedbi s I/O operacijama koje parser ionako obavlja, a pretvara tiho oštećenje memorije u uhvatljivu ERangeError pogrešku
Treće je ponašanje metode TMemoryStream.SetSize uz Int64 isporučen iz datoteke. U trenutnom RTL-u alocira se što god je datoteka tražila, pa jedan tok koji traži četiri gigabajta postaje greška manjka memorije usred prihvata. U starijim RTL-ovima, gdje SetSize prima Longint, vrijednost se prvo tiho sužava: deklariranih $100000010 postaje 16, alokacija uspijeva, a zapisivanje stvarnih podataka se nastavlja daleko izvan njezinih okvira. Potvrdite valjanost svake veličine u odnosu na izmjerenu izvornu veličinu i striktno ograničenje prije nego to vidi bilo koji poziv za alokaciju
Pomaci koji pokazuju izvan datoteke
Tablica unakrsnih referenci mapira brojeve objekata na apsolutne pomake u bajtovima, a parser skače tamo gdje god to pokazuje. U oštećenoj ili neprijateljskoj datoteci ti pomaci slete iza kraja datoteke ili unutar nepovezanih struktura. TStream čini ovaj kvar tihim: postavljanje Position iza Size nije pogreška, a običan poziv Read nakon kraja jednostavno vraća manje bajtova od zatraženih, pa kod koji preskače provjeru broja nastavlja parsirati zaostale bajtove iz prethodnog objekta. Obrana se sastoji od tzv. chokepointa — jedne pomoćne funkcije kroz koju prolazi svako pomicanje i čitanje vođeno datotekom, a koja provjerava valjanost pomaka i brojača u odnosu na izmjerenu veličinu datoteke prije nego što se tok uopće pomakne
uses
System.SysUtils, System.Classes;
const
MAX_OBJECT_BYTES = 64 * 1024 * 1024; // no single object may exceed 64 MB
type
EPdfBoundsError = class(Exception);
// Every file-driven seek and read goes through here. Offset and Count are
// file-supplied claims; Source.Size is the measurement they must fit.
procedure ReadBounded(Source: TStream; Offset, Count: Int64;
var Buffer: TBytes);
begin
if (Offset < 0) or (Count < 0) or (Count > MAX_OBJECT_BYTES) or
(Offset > Source.Size) or (Count > Source.Size - Offset) then
raise EPdfBoundsError.CreateFmt(
'object extent %d+%d exceeds file size %d',
[Offset, Count, Source.Size]);
SetLength(Buffer, Count);
if Count = 0 then
Exit;
Source.Position := Offset;
Source.ReadBuffer(Buffer[0], Count);
end;
Usmjerite sve pomake unakrsnih referenci, raspona tokova i čitanja ugrađenih datoteka kroz njega, pa će loš pomak postati čisto odbacivanje koje navodi te brojeve umjesto kršenja prava pristupa koje bi se dogodilo tri poziva poslije
Ciklusi i dubina unutar grafa objekata
PDF je graf, a ne stablo. Bilo koja vrijednost može biti neizravna referenca, referenca se može razriješiti u drugu referencu — /Length 12 0 R, gdje objekt 12 sadrži 13 0 R — i ništa ne sprječava da se lanac ponovno zatvori sam u sebe. Razrješavač koji prati reference naivno ponavlja rekurziju sve dok se ne iscrpi izvorni stog, a iscrpljivanje stoga nije nešto što jednostavno možete uloviti; to završava proces. Duboko ugniježđeni nizovi i rječnici dolaze do istog kraja čak i bez ikakvog ciklusa
Koristite dvije zaštite zajedno: eksplicitni brojač dubine ograničava pošten-ali-dubok slučaj do granice kojoj se ne približava nijedna legitimna datoteka, a skup posjećenih hvata istinski ciklus pri njegovom drugom posjetu, pretvarajući to u preciznu pogrešku koju je moguće prijaviti umjesto u jednostavni pad zbog ograničenja
uses
System.SysUtils, System.Generics.Collections;
const
MAX_RESOLVE_DEPTH = 32; // far deeper than any legitimate reference chain
type
EPdfStructureError = class(Exception);
TPdfValueKind = (pvNull, pvNumber, pvName, pvString, pvArray,
pvDictionary, pvStream, pvReference);
TPdfValue = record
Kind: TPdfValueKind;
RefNumber: Integer; // meaningful when Kind = pvReference
// ... payload fields for the remaining kinds
end;
// LoadObject is your own routine: it looks up the xref offset for
// ObjNumber, reads the object with ReadBounded, and parses it.
function ResolveObject(ObjNumber, Depth: Integer;
Visited: TDictionary<Integer, Boolean>): TPdfValue;
begin
if Depth > MAX_RESOLVE_DEPTH then
raise EPdfStructureError.Create('reference chain exceeds depth limit');
if Visited.ContainsKey(ObjNumber) then
raise EPdfStructureError.CreateFmt(
'circular reference through object %d', [ObjNumber]);
Visited.Add(ObjNumber, True);
try
Result := LoadObject(ObjNumber);
if Result.Kind = pvReference then // e.g. /Length 12 0 R
Result := ResolveObject(Result.RefNumber, Depth + 1, Visited);
finally
Visited.Remove(ObjNumber); // siblings may legally share this object
end;
end;
Dekompresija služi kao pojačivač
Nekoliko kilobajta FlateDecode unosa može se napuhati do razine gigabajta; kompresija opće namjene nagrađuje ponavljajući čisti tekst, a napadač ga može učiniti maksimalno ponavljajućim. Ograničite napuhanu veličinu svakog toka na ono što njegovom potrošaču zapravo može trebati i vodite drugi proračun po dokumentu: pet stotina tokova od kojih je svaki tik ispod gornje granice za svaki pojedini tok iscrpljuje memoriju jednako sigurno kao i jedan divovski tok. Provjera pripada unutar same petlje inflacije, na način da broji izlazne bajtove kako se proizvode i prekida čim nastupi probijanje, a ne nakon petlje kada je memorija već potrošena. Proračun dokumenta izražen kao višekratnik veličine komprimirane datoteke dobro funkcionira, budući da se legitimni dokumenti grupiraju daleko ispod omjera koji postiže posebno kreiran namjerni tok
Dubinska obrana izvan vaših vlastitih jedinica
Iste klase kvarova žive unutar biblioteka. Dvije studije slučaja na ovom blogu prolaze kroz stvarne slučajeve: preklapanje cjelobrojnih vrijednosti, neograničena rekurzija i neinicijalizirani međuspremnici popravljeni su u izvornom Pascal engineu unutar članka Ojačavanje Pascal PDF parsera protiv zlonamjernih datoteka, kao i opasnosti konvencija pozivanja, širine cjelobrojnih vrijednosti te vlasništva kod povezivanja C enginea u članku Ojačavanje vezivanja PDFium komponente. Za doista nepouzdani unos — obrazac za javni prijenos ili neautentificirani poštanski sandučić — također pokrenite rad parsiranja i dekodiranja u zasebnom procesu niske razine privilegija, tako da datoteka koja pobijedi svaku in-process zaštitu u konačnici košta samo kao neuspjeli posao umjesto kao ugašen servis
Pripremna kontrolna lista
Prije nego sljedeći build krene u isporuku, prođite kroz ovaj popis provjera za parser: svaki međuspremnik toka dimenzioniran na temelju strogo ograničene duljine umjesto one deklarirane; svaki raster dimenzioniran prema potvrdanim parametrima dekodera i uspoređen s izlazom dekodera; svaki umnožak dimenzija procijenjen u Int64 te uspoređen s izričitim gornjim ograničenjem; aktivan {$R+} u svakoj jedinici koja se indeksira pomoću vrijednosti izvedenih iz datoteke; skokovi s provjerenim granicama u odnosu na izmjerenu veličinu datoteke; ograničena dubina za svako razrješavanje reference kao i obavljena provjera ciklusa; i to da svaka inflacijska petlja broji izlaze u odnosu na proračune po pojedinačnom toku te po cijelom dokumentu. Nijedna od ovih provjera ne troši mjerljivo vrijeme na legitimnom dokumentu, a svaka pretvara kvarenje memorije u čisto odbacivanje koje se lako može zabilježiti u log
Napomena: losLabove komponente HotPDF Component, PDFlibPas Delphi PDF Library te PDFium Component primjenjuju ove provjere granica, ograničenja dubine te ograničenja širenja na unutarnjoj razini, tako da prihvatni cjevovod izgrađen na njima započinje od početne točke koja je već snažno osigurana