Jalur asupan (intake) dokumen menerima file yang ditulis oleh orang tak dikenal. Faktur, hasil pindai, lampiran dari sebuah formulir web: masing-masing mengklaim sebagai PDF dan membawa ratusan angka yang diharapkan menjadi sandaran kerja bagi pengurai Anda. Panjang aliran, dimensi gambar, offset byte, referensi objek — masing-masing dipilih oleh siapa pun yang menghasilkan file tersebut, dan sebuah unggahan terpotong atau dokumen yang sengaja dibuat cacat sewaktu-waktu akan meletakkan salah satu dari angka tersebut pada titik di mana ia bakal menorehkan kerusakan. Perbedaan antara sebuah pengurai yang bertahan hidup dari serangan file semacam itu dan yang hancur, atau yang tetap berjalan dengan memori terkorupsi, merupakan serangkaian kecil kebiasaan yang tak melulu harus bergantung pada sebuah pustaka PDF tertentu
Kebiasaan-kebiasaan tersebut membagikan satu pijakan (premise): sebuah nilai yang dibaca dari file adalah suatu klaim (tuntutan), bukannya sebuah hitungan hasil-ukur (measurement). Hal itu menjadi absah untuk dipakai semata sesudah ia diperiksa terhadap sesuatu yang sanggup diukur sendiri dengan tangannya oleh si pengurai itu kelak — ukuran file yang sesungguhnya, jumlah per-byte yang dihasilkan oleh dekoder yang nyatanya, atau kedalaman sejati dari sebuah rekursi. Apa yang menyusul kemudian merupakan pijakan tersebut yang diaplikasikan pada tempat-tempat di mana para pengurai dokumen pada kenyataannya patah (crashes)
Sebuah panjang yang dideklarasikan adalah sebuah klaim, bukan pengukuran
Ketidakcocokan paling sederhana adalah panjang aliran. Objek aliran PDF mendeklarasikan jumlah byte-nya dalam kunci /Length, dan data aktual berada di antara kata kunci stream dan endstream. Tidak ada yang memaksa keduanya untuk akur. File yang terpotong memuat byte nyata yang lebih sedikit dari jumlah yang dideklarasikan; file dari generator yang rusak dapat mendeklarasikan panjang yang menjangkau melewati akhir file atau masuk ke dalam objek tetangga. Alokasikan dari nilai yang dideklarasikan dan salin sampai endstream maka Anda akan meluapkan buffer; baca tepat seperti hitungan deklarasinya tanpa mengecek ketersediaannya dan Anda akan berjalan melewati ujung file. Biarkan nilai deklarasi mengemudikan alokasi hanya sesudah ia dijepit (clamped) dengan jarak ukur terhadap batas ujung data, lalu tangani sebuah silang-selisih selaku suatu titik keputusan (decision point) — perbaikilah lewat memindai buat memburu endstream, ataupun tolak aliran tersebut — jangan memercayainya secara bulat-bulat
Parameter gambar yang mendeskripsikan raster yang lebih besar dari yang Anda alokasikan
Aliran gambar menaikkan pertaruhan karena dua himpunan angka independen mendeskripsikan piksel yang sama. Kamus gambar membawa /Width dan /Height, dan buffer raster biasanya berukuran berdasarkan angka tersebut. Filter dekode membawa geometrinya sendiri: CCITTFaxDecode mengambil /Columns, /Rows, dan /K dari DecodeParms-nya, di mana /K memilih skema Grup 3 atau Grup 4 dan dekodernya memancarkan (Columns + 7) div 8 byte per baris pindai. Sebuah file yang mendeklarasikan /Width 100 tetapi menyerahkan kepada filter tersebut /Columns 1728 — nilai baku (default) — akan menjadikan dekoder memproduksi lebih dari enam belas kali lipat byte per baris-pindai ketimbang yang diantisipasi oleh buffernya, lalu limpahan tersebut mendarat meraup satu demi satu baris-pindai pada apapun jua yang bersemayam sesudah batas dari alokasi awalnya. Di kala /Rows absen maka dekodernya berlari kencang hingga pada saat datanya menyerukan stop, lantas oleh karena hal itu pagarilah batasan jumlah dari baris-baris tersebut juga. DCTDecode memiliki kelindan celah-kampuh (seam) yang identik rupanya: data JPEG membawa parameter lebar dan tingginya tersendiri di dalam markah SOF, dan tiada satupun aturan yang memaksa mereka untuk pas mencocokkan diri bersama sang kamusnya
Aturan pertahanan tersebut bersifat mekanis: hitung ukuran raster yang diharapkan dari parameter dekode yang tervalidasi — /Columns dan /Rows dari filter itu sendiri untuk CCITT, dimensi SOF untuk DCT — periksa terhadap batasan Anda, alokasikan dari nilai tersebut, dan verifikasi selama dekode bahwa keluaran tidak pernah melampaui alokasi. Saat kamus dan filter tidak setuju mengenai geometri, damaikan keduanya atau tolak gambar tersebut. Apa yang sama sekali tidak boleh dilakukan sebuah pengurai adalah mengatur ukuran buffer dari satu kumpulan angka lalu membiarkan dekodernya berjalan berdasarkan angka yang lain
Jebakan aritmetika dan alokasi di Delphi
Tiga perilaku Delphi melemahkan bahkan sebuah pengurai yang berniat untuk memvalidasi. Yang pertama adalah perkalian 32-bit: Delphi mengevaluasi hasil dari dua operan Integer pada 32 bit tanpa mempedulikan lebar tujuannya, sehingga Width * Height * BytesPerPixel bisa tergulung (wrap) kembali meskipun setiap faktor lulus pemeriksaan kewarasannya (sanity check) masing-masing. Sebuah pemindaian 30000 kali 30000 pada tiga byte per piksel berarti 2,7 miliar byte, yang akan menggulung-negatif dalam aritmetika 32-bit bertanda; kombinasi faktor-faktor yang sedikit berbeda akan tergulung menjadi sebuah angka positif kecil yang lalu mengalokasikan dan menjadikan buffer itu terlalu-kecil (undersized). Paksakan keseluruhan ekspresi tersebut menjadi lebar dengan melakukan cor-tipe (cast) pada operan pertamanya — Size := Int64(Width) * Height * BytesPerPixel — kemudian bandingkan angka tersebut terhadap sebuah batas eksplisit sebelum apapun menyentuh SetLength
Yang kedua adalah pemeriksaan rentang (range checking). Konfigurasi rilis bawaan Delphi hadir dengan pemeriksaan tersebut dalam keadaan mati, jadi indeks di luar rentang (out-of-range) yang dihitung dari data file tak akan memunculkan apa-apa — indeks itu membaca atau menulis ke memori yang berdampingan dengan lariknya (array). Hidupkan kembali dengan {$R+} (dan {$Q+} untuk limpahan/overflow aritmetika) pada awal dari setiap unit yang mengindeks melalui nilai-nilai yang ditarik dari file. Besaran biayanya tak terukur secara bermakna di hadapan aktivitas I/O yang pasti dikerjakan oleh sebuah pengurai secara awamnya, lalu ia akan menukarkan (converts) tindakan perusakan senyap (silent corruption) itu menjadi suatu tangkapan ERangeError
Yang ketiga adalah TMemoryStream.SetSize dengan pasokan Int64 dari file. Pada RTL yang mutakhir, metode itu akan menuruti seberapapun ukuran permintaan filenya, hingga satu aliran yang menyeru mengklaim permohonan empat gigabyte niscaya akan menjadi kegagalan out-of-memory murni di separuh jalan (mid-intake). Di versi-versi RTL uzur, di mana SetSize menggenggam asupan Longint, angkanya bakal tanpa bunyi-bunyi disusutkan-sempit pada awalnya: sebuah deklarasi $100000010 diubah jadi bernilai 16, proses alokasinya melaju mulus, lalu penulisan data sejatinya meluap lari lepas berlari melewatinya jauh di depannya. Sahkan tiap ukur besarnya berlandaskan set hasil pengukuran ukuran per-sumbernya serta sebiji batas atas kaku (hard cap) sebelum sekian rentetan seruan permohonan alokasinya melihat nilainya itu jua
Offset yang menunjuk ke luar file
Tabel referensi-silang memetakan nomor objek ke offset byte absolut, dan pengurai mencari (seeks) ke mana pun ia menunjuk. Dalam file yang rusak atau merusak (hostile), offset tersebut mendarat melewati batas akhir file atau di dalam struktur yang tidak terkait. TStream menjadikan kegagalan itu sunyi: mengatur Position melampaui Size bukanlah sebuah galat (error), dan sebuah Read biasa yang melampaui bagian akhir (past the end) sekadar mengembalikan byte yang lebih sedikit ketimbang yang diminta, jadi kode yang melompati proses pemeriksaan jumlah (count check) akan terus mengurai sisa byte yang basi (stale) dari objek sebelumnya. Pertahanannya adalah sebuah titik sempit (chokepoint) — satu pembantu yang dilewati oleh setiap pencarian dan pembacaan yang digerakkan oleh file, yang memvalidasi offset dan jumlah terhadap ukuran file yang diukur sebelum alirannya bergerak
uses
System.SysUtils, System.Classes;
const
MAX_OBJECT_BYTES = 64 * 1024 * 1024; // tidak ada objek tunggal yang boleh melebihi 64 MB
type
EPdfBoundsError = class(Exception);
// Setiap pencarian (seek) dan pembacaan (read) yang didorong oleh file melewati sini. Offset dan Count adalah
// klaim-klaim yang dipasok oleh file; Source.Size adalah pengukuran tempat mereka harus pas (sesuai).
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(
'luasan objek %d+%d melampaui ukuran file %d',
[Offset, Count, Source.Size]);
SetLength(Buffer, Count);
if Count = 0 then
Exit;
Source.Position := Offset;
Source.ReadBuffer(Buffer[0], Count);
end;
Rutekan offset referensi-silang, luasan aliran, dan pembacaan file-tertanam melaluinya, dan offset yang buruk berubah menjadi penolakan rapi yang menyebutkan angkanya alih-alih pelanggaran akses (access violation) tiga panggilan sesudahnya
Siklus dan kedalaman di dalam graf objek
Sebuah PDF merupakan sebuah graf, bukan sebuah pohon. Sembarang nilai bisa jadi merupakan referensi tak-langsung (indirect reference), sebuah referensi dapat menyelesaikan (resolves) dirinya merujuk kepada sebuah referensi lainnya — /Length 12 0 R, di mana objek 12 memuat 13 0 R — dan tiada apa pun yang merintangi rantai tersebut untuk menutup siklusnya (berputar kembali) ke dirinya sendiri. Sebuah pemecah-resolusi (resolver) yang menelusuri rentetan rujukan secara naif akan merekursi berulang-ulang sampai tumpukan asli (native stack) bawaannya terkuras ludes kandas habis (exhausted), dan kehabisan memori tumpukan tersebut bukanlah suatu hal yang bisa ditangkap (catch); hal tersebut mematikan prosesnya secara seketika. Matriks (arrays) maupun kamus (dictionaries) yang tersarang (nested) dengan sedemikian dalam akan menyentuh kesudahan yang sama sekalipun tanpa adanya sebuah wujud putaran siklus sama sekali
Gunakan dua penjagaan bersama-sama: sebuah konter kedalaman (depth counter) eksplisit untuk membatasi kasus jujur-tapi-dalam pada limit yang tiada satu pun file wajar berpeluang mencapainya, serta suatu himpunan yang-pernah-dikunjungi (visited set) guna mendeteksi siklus tulen (genuine cycle) sewaktu kunjungan keduanya berulang, meretas merombak perwujudannya menjadi sebentuk error galat yang secara presisi mampu direkam sebagai laporan, bukannya sekadar lilitan limit yang tergelincir berantai-antai
uses
System.SysUtils, System.Generics.Collections;
const
MAX_RESOLVE_DEPTH = 32; // jauh lebih dalam dari rantai referensi logis apa pun
type
EPdfStructureError = class(Exception);
TPdfValueKind = (pvNull, pvNumber, pvName, pvString, pvArray,
pvDictionary, pvStream, pvReference);
TPdfValue = record
Kind: TPdfValueKind;
RefNumber: Integer; // berarti saat Kind = pvReference
// ... bidang muatan untuk jenis yang tersisa
end;
// LoadObject adalah rutinitas Anda sendiri: ia mencari offset xref untuk
// ObjNumber, membaca objek tersebut dengan ReadBounded, dan mengurainya.
function ResolveObject(ObjNumber, Depth: Integer;
Visited: TDictionary<Integer, Boolean>): TPdfValue;
begin
if Depth > MAX_RESOLVE_DEPTH then
raise EPdfStructureError.Create('rantai referensi melampaui batas kedalaman');
if Visited.ContainsKey(ObjNumber) then
raise EPdfStructureError.CreateFmt(
'referensi sirkuler melalui objek %d', [ObjNumber]);
Visited.Add(ObjNumber, True);
try
Result := LoadObject(ObjNumber);
if Result.Kind = pvReference then // mis. /Length 12 0 R
Result := ResolveObject(Result.RefNumber, Depth + 1, Visited);
finally
Visited.Remove(ObjNumber); // kerabat mungkin secara sah membagikan objek ini
end;
end;
Dekompresi adalah sebuah amplifier
Beberapa kilobyte asupan FlateDecode dapat mengembang (inflate) menjadi gigabyte; kompresi tujuan-umum menghargai (rewards) teks datar yang berulang, dan seorang penyerang dapat menjadikannya maksimal secara berulang. Batasi ukuran pengembangan dari setiap aliran pada nilai yang masuk akal yang dibutuhkan oleh konsumennya, dan pertahankan anggaran kedua per-dokumen: lima ratus aliran yang masing-masing tepat di bawah ambang batas per-aliran akan menguras memori secepat sebuah aliran raksasa tunggal. Pengecekan itu sejatinya wajib ditempatkan di dalam perulangan pengembang-mekar (inflation loop) tersebut, menghitung kelipatan byte-byte luaran yang dihasilkan seraya seketika menggugurkannya tatkala menabrak batas, bukannya menunggu di luar siklus perulangannya usai tatkala ranah per-memori sudah terlebih dahulu kandas ludes. Sebuah takaran porsi (budget) untuk dokumen dapat ditetapkan berupa nilai angka per-kelipatan muatan wujud ukuran dari rupa kompresi file-nya dengan merupa tingkat efektivitas lurus set sangat memuaskan laik sudi jalan yang lancarnya kelak, sebab wujud kumpulan wujud set-sajian dokumen-dokumen yang valid (legitimate documents) nyatanya menumpuk bergelumbul mengerumuni (cluster) wilayah area di jauh merentang bawah luaran angka-rasio rasio kompresi ujud-wujud buatan ujud file luaran jahat merupa buatan-sengaja (crafted stream) murninya
Pertahanan berlapis (defense in depth) di luar unit-unit Anda sendiri
Kelas kecacatan (defect) yang sama hidup di dalam pustaka-pustaka (libraries). Dua studi kasus pada blog ini merunut langkah-langkah kejadian nyatanya secara runut: penggulungan bilangan (integer wraps), rekursi nirbatas (unbounded recursion), beserta celah buffer tanpa inisialisasi pada mesin berbasis Pascal native dapat Anda telusuri dalam Mengeraskan Pengurai PDF Pascal Terhadap File Berbahaya, di samping bahaya kepemilikan (ownership hazards), ukuran lebar-integer, dan konvensi-pemanggilan (calling-convention) dari proses mengikat (binding) mesin C dituang pada Mengeraskan Pengikatan Komponen PDFium. Diperuntukkan secara murni bagi raupan proses-masukan serapan yang secara telak tak-dapat-dipercaya set-seutuhnya (untrusted intake) — semacam formulir unduhan ter-publik unggahan-umum, hingga kotak surat-surel tanpa pen-sahan laju otentikasi (unauthenticated mailbox) — operasikan eksekusi rupa alur dari kerja-penguraian (parse) dengan dekode di lingkup satu entitas-proses (process) ber-hak istimewa rendah (low-privilege) luaran wujud-terpisah wujud murni tersendiri-di luarnya, yang mana dengan siasat raupan mutlak itu se-seutas dari pias luaran se-file wujud raupan berkas yang kelak sanggup rupa mempecundangi menjatuhkan melumpuhkan segenap benteng lapis (guard) dalam-proses (in-process) tersebut akan memulangkan kerugian ongkos berupa satu tugas (job) batal lurus merupa ujud gagal, bukannya harus membayarnya mutlak luaran laju rupa dengan wujud murni se-matinya roboh rupa layanan (service) sudi rupa tumbang luaran secara mutlak-total ujud seutuhnya
Sebuah daftar periksa (checklist) prapenerbangan
Sebelum rilis build berikutnya diluncurkan, telusuri (walk) pengurainya terhadap daftar ini: setiap buffer aliran ukurannya diambil dari panjang yang dijepit (clamped) ketimbang angka deklarasinya; tiap rupa raster dibesarkan wujud dengan luaran bertumpu parameter dekoder yang sudah ter-sahkan diiringi lajur kros-cek melawan (against) laju-luaran keluarannya dekoder rupa luaran dari dekodernya; setiap hasil kali dari serapan kalkulasi produk set per-dimensi kelindan per-kali (dimension product) murni rupa dikalkulasikan (evaluated) luaran dalam ruang lingkup ujud ranah murni Int64 beserta disanding disamakan diperlawankan purna merupa luaran lajur se-dikomparasikan lurus membanding ditanding merupa di-tanding pias luaran dibanding (compared) rupa lurus membandingkannya kepada sesosok pias kelindan se-patok puncak wujud raupan-batas luaran (explicit cap); lalu pastikan kelindan murni pias {$R+} hidup (active) berkibar rupa murni serapan laju di selingkup se-saban setiap ujud laras dari saban wujud-per-unit yang murni yang mana sudi ber-operasi melajur se-mengindeks (indexes) bernaung dengan serapan wujud lurus dari pasokan wujud angka nilai rupa angka-turunan luaran-terambil-dari (file-derived values) raupan set file; tiada setitik kelindan rupa luaran pencarian se-jejak serapan cari per-baca ujud-mencari luaran wujud lurus murni luaran per-satu cari pencarian laras (seek) yang luaran tidak purna lurus diper-selidik rupa sudi diper-tangkal rupa set-dibatasi per-cegat pias-dihadang per-batasi sudi (bounds-checked) menentang lajur muatan hasil ukur wujud ukuran aslinya muat ukuran aslinya ukuran dari wujud si sang file mutlak ukurnya laras merupa ukuran; tiada murni luaran seutas wujud penyelesaian ujud serap rupa mutlak memecah-simpul per-se-simpul rupa luaran membedah menelaah menyelesaikannya se-pemecahan ujud-resolusi mutlak serapan luaran wujud per-resolusi laju serap dari referensi kelindan mutlak tiada yang luaran murni per-niscaya purna se-yang mutlak set-tiada luaran murni se-tidak laju serapan disekat kelindan dibatasi laras dibatasi-kedalamannya sudi lurus (depth-limited) di-temani laras merupa kelindan ditimpali mutlak luaran laras dan serap mutlak sudi dipindai diperiksa disidik diselisik siklus putar rupa putaran pias ujud serapan mutlak siklus-diperiksanya (cycle-checked); luaran tiap purna serapan rupa wujud muatan luaran laju pias luaran dari selingkup laras putaran raupan pengembang wujud ikal pias-ikal luaran membesar memuai melebarnya laras wujud laju dari pengembang-pemuai mekar rupa mengembang (inflation loop) luaran niscaya mutlak lajunya wajib murni per-rutin serapan ter-awasi lurus mutlak muat cacah membukukan mem-buku laras serapan luaran ujud murni mengumpulkan-hitungan se-cacah luaran-menghitung (counting) merunut keluaran serapan luaran se-rincian luaran byte kelindan wujud ujud mutlak (output) ditimpakan disarangkan menyandar purna membentur se-membidik murni se-berlandas (against) setali lurus luaran wujud se-hadap terhadap laras muat raupan dari per-aliran pias set aliran rupa-per aliran wujud luaran ujud dari dan kelindan se-serta murni per-dokumen serapan luaran anggaran (budgets). Tak rupa set-satupun kelindan wujud ujud mutlak dari lajur raupan dari-pemeriksaan luaran pemeriksaan-pemeriksaan pias ujud tersebut luaran murni memakan rupa menagih ongkos menyedot mutlak se-meminta harga luaran wujud mematok-biaya luaran (costs) muat waktu rentang set wujud se-waktu purna murni luaran yang dapat dirasakan se-dapat-diukur pias luaran ter-ukur (measurable) rupa di atas raupan seutas file sah lurus dokumen luaran dokumen-yang-sah ujud-dokumen-sah purna merupa murni (legitimate document), serta masing-masing purna luaran se-setiap satunya murni luaran purna melurus wujud menyulap mengubah luaran ujud-merupa dari-wujud menukarkan (converts) pias kelindan se-korupsi-memori purna luaran mutlak dari kerusakan luaran memori-rusak rupa se-korupsi luaran memori merupa rupa luaran set-ke luaran ke wujud dalam rupa ujud purna per-bentuk luaran wujud set-dalam ujud serapan purna se-sebuah kelindan raupan-rupa luaran penolakan murni ujud penolak lurus se-penolakan (rejection) murni yang rapi luaran sudi purna bisa-direkam sanggup rapi bersih mutlak luaran yang-bersih pias luaran wujud bersih serapan dapat mutlak sudi wujud dicatatkan kelindan log lurus se-dicatat-log luaran sanggup-dilog (loggable) laras kelak purnanya asalnya murninya
Catatan: Komponen HotPDF, Pustaka PDF Delphi PDFlibPas, dan Komponen PDFium milik losLab menerapkan pemeriksaan batas, batasan kedalaman, dan batas atas pengembangan ini secara internal, sehingga sebuah jalur perpipaan asupan yang dibangun di atasnya memulai perjalanannya bertolak dari suatu pangkalan fondasi dasar (baseline) yang sudah diperkeras secara tangguh semenjak lahirnya