Bir hizmet sürecini OOM killer onu alana kadar kilitleyen 20 KB'lık bir PDF, kodunuzdaki bir hata değildir, bir sıkıştırma açma bombasıdır. Delphi ve C++Builder için yerel VCL PDF bileşeni olan HotPDF, bunu varsayılan olarak 268435456 byte'a ayarlı ve her decode aşamasını tek bir paylaşılan bütçeye karşı ücretlendiren filtre-zinciri-başına bir tavan olan DecodeBudgetBytes ile sınırlar
Bir worker süreci yiyen 20 KB'lık dosya
Olayın şekli her zaman aynıdır. Küçük resimler render eden bir kuyruk worker'ı bir yükleme alır, yerleşik bellek iki saniyeden kısa sürede 12 GB'ı aşar ve süreç yığın izi olmadan kaybolur. Dosya 20 KB'dır. Bir sayfası, bir içerik akışı ve beş girdili bir /Filter dizisi vardır. Dizideki her isim spesifikasyonun tanımladığı bir filtredir, her aşama hatasız decode olur ve dosyada biçimsiz hiçbir şey yoktur. Bu sınıf girdiyi garip kılan şey budur: reddedilecek bozuk bir bayt yoktur
Bu, tek bir filtreyi doğru şekilde decode etmekle aynı problem değildir. LZWDecode'u ve /DecodeParms öngörücüsünü doğru yapmak kendi başına bir konudur, yüklü belgelerde LZW, öngörücüler ve DecodeParms üzerine anlatımda ele alınmıştır. Burada her decoder zaten doğrudur. Hata, doğru decoder'ların beşini art arda çalıştırdığınızda ve kimse toplamı saymadığında ne yaptıklarıdır. ISO 32000-1 §7.4, /Filter'ın tek bir isim veya isimler dizisi olabileceğini ve bir dizinin sırayla, önce ilk girdi olmak üzere uygulandığını açıkça belirtir. Bir aşamanın girdisini ne kadar genişletebileceği hakkında hiçbir şey söylemez, zincir boyunca toplam hakkında da hiçbir şey söylemez. Bir ASCIIHexDecode aşaması, zararsız gibi görünen, girdisini kabaca yarıya indirir. Bir dizi sıfır bayt üzerinde bir FlateDecode aşaması binlerce oranına ulaşır. Bunları zincirleyin ve aritmetik çarpımsal olur: 20 KB, 20 MB olur, 20 GB olur ve her bireysel adım yasal bir akışın uyumlu bir decode'udur
Filtre başına bir sınır bir decode bombasını neden durduramaz?
Çünkü filtre başına bir sınır, /Filter dizisinin her elemanında yeniden silahlanır. 256 MiB aşama-başına tavan altındaki beş aşamalık bir zincir 1,25 GiB'yi yetkilendirir ve son aşama, öncesindeki dördü ne üretmiş olursa olsun yine de tamamen taze bir ödenekle başlar. Sınır dürüstçe uygulanır ve önemli olan hiçbir şeyi kısıtlamaz. HotPDF, v2.447.0'dan önce tam olarak bu şekle sahipti ve yanında ikinci bir boşluk daha vardı. LZW sıkıştırma açıcı bir MaxOutputBytes tavanı taşıyordu ve görüntü öngörücü yolu kendi satırlarını hesaba katıyordu, dolayısıyla bu ikisi yerel olarak sınırlıydı. FlateDecode, ASCIIHexDecode, ASCII85Decode ve RunLengthDecode'un hiç tavanı yoktu: her biri, girdisi bitene veya ayırıcı pes edene kadar bir TMemoryStream'e yazıyordu. Dolayısıyla düşman bir zincirin iki geçiş yolu vardı. Tamamen korumasız bir filtre kullanabilir veya korumalı olanları kullanıp basitçe daha fazlasını ekleyebilirdi
Saf bir düzeltmenin kaçırdığı üçüncü bir ayrıntı var. Önem verdiğiniz sayı, son decode edilmiş çıktının boyutu değildir. Bu, doruktur ve dorukta genellikle bir ara arabellekte yaşar. Mütevazı bir 4 MB içerik akışıyla biten bir zincir, üçüncü aşamada 8 GB tahsis edebilir ve tamamen makul görünen bir şeyi geri verebilir. Sonuçtaki uzunluğu daha sonra kontrol etmek size süreci öldüren tahsis hakkında hiçbir şey söylemez
Filtre zinciri başına bir bütçe izleyici
HotPDF v2.447.0'daki düzeltme, muhasebeyi aşama yerine zincir boyunca yaymaktır. Her filtre zinciri bir THPDFDecodeBudgetTracker inşa eder ve her decoder, gerçek hedefi saran bir THPDFBudgetWriteStream aracılığıyla yazar. Sarmalayıcı, tek bir baytı yönlendirmeden önce Budget.Consume(Count)'u çağırır, dolayısıyla ret, hedef akış hâlâ eski boyutundayken gerçekleşir. Bu sıralama tüm amaçtır: arabellek zaten büyüdükten sonra yapılan bir kontrol bir savunma değil, bir tanıdır
// Simplified from the HotPDF chain decoder: one tracker for the whole
// /Filter array, one bounded wrapper stream per stage
Budget := THPDFDecodeBudgetTracker.Create(Doc.DecodeBudgetBytes);
try
for I := 0 to FilterCount - 1 do
begin
if I = 0 then
InputStream := StreamObj.Stream // read the source, do not copy it
else
InputStream := CurrentStream;
NextStream := TMemoryStream.Create;
InputStream.Position := 0;
// BeginFilter names the stage and bumps FilterCount; the wrapper
// stream calls Budget.Consume before writing into NextStream
Doc.LoadUnFlateLZW(InputStream, NextStream, Filters[I], True, Budget);
CurrentStream.Free;
CurrentStream := NextStream;
end;
finally
Budget.FinishFilter;
Budget.Free;
end;
Yerel tavanlar ortadan kalkmadı, paylaşılan bütçenin izdüşümleri hâline geldi. LZW aşaması artık Decoder.MaxOutputBytes := Budget.RemainingBytes'i ayarlar, dolayısıyla özel tavanı bağımsız bir ödenek yerine zincirin ne kadar kalmışsa odur. Görüntü öngörücü aşaması BeginFilter ile açılır ve tahsis etmeden önce satır gereksinimini Consume üzerinden ücretlendirir, bu da öngörücü çıktısının, onu besleyen genel filtrelerle aynı bütçeye fatura edildiği anlamına gelir. Bu özellikle, filtre zinciri ve öngörücünün tek bir işlemin iki yarısı olduğu görüntü yolunda önemlidir, yüklü belgelerden decode filtreleri üzerinden görüntü çıkarmada ele alındığı gibi
Bütçe reddettiğinde çağıran ne görür?
Yığının en altında, bir ret EHPDFDecodeBudgetError fırlatır. Onun üstünde, cevap, çağrılan API'nin zaten sahip olduğu sözleşmeye bağlıdır. Başarısızlığı False veya nil üzerinden bildiren üst düzey okuma yöntemleri tam olarak bunu yapmaya devam eder, çünkü belgelenmiş bir boolean sonucu bir istisnaya dönüştürmek, biçimsiz girdiyi zaten doğru şekilde ele alan çağıranları bozardı. Yüklü sayfa içeriği yolu kasıtlı bir istisnadır: kesilmiş bir içerik akışının yalnızca boş çıkan bir sayfa olarak render edilmesine izin vermek yerine EHPDFDecodeBudgetError'ı yeniden fırlatır. Bu tasarım, çıplak bir False'un tek başına belirsiz olduğu anlamına gelir, dolayısıyla bütçe onun yanında bir tanı kaydı yayınlar: THotPDF.GetLastDecodeBudgetInfo, örneğin decode ettiği en son zincirin durumunu döndürür
type
THPDFDecodeBudgetInfo = record
LimitBytes: Int64;
DecodedBytes: Int64;
PeakStageBytes: Int64;
FilterCount: Integer;
Exceeded: Boolean;
ExceededFilter: AnsiString;
end;
var
Pdf: THotPDF;
Info: THPDFDecodeBudgetInfo;
PageText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.DecodeBudgetBytes := 64 * 1024 * 1024; // tighter than the default
Pdf.LoadFromFile('untrusted.pdf');
if not Pdf.ExtractLoadedPageText(0, PageText) then
if Pdf.GetLastDecodeBudgetInfo(Info) and Info.Exceeded then
LogWarning(Format(
'decode refused in %s after %d bytes, peak stage %d, %d filters',
[String(Info.ExceededFilter), Info.DecodedBytes,
Info.PeakStageBytes, Info.FilterCount]));
finally
Pdf.Free;
end;
end;
Bu alanları birlikte okuyun, iki saldırı biçimini ayırırlar. PeakStageBytes, DecodedBytes'e yakın olduğunda, tek bir aşama tüm hasarı yapmıştır ve tek bir yüksek oranlı filtreye bakıyorsunuzdur. PeakStageBytes, DecodedBytes'in küçük bir kesri olduğunda ve FilterCount yüksek olduğunda, hiçbir bireysel aşama aşırı değildi ve zincir tavanın ötesine birikerek geçti, ki bu tam olarak filtre başına bir sınırın göremediği durumdur. Handler'ınıza yazmaya değer bir uyarı: GetLastDecodeBudgetInfo, örnek en az bir filtre decode edene kadar False döndürür, dolayısıyla ondan gelen bir False, belgenin temiz olduğuna dair kanıt değildir
Bütçenin sıfırlandığı yer ve sıfırın dürüst cevap olduğu zaman
DecodeBudgetBytes, bir belgeyi değil, bir akış zincirini sınırlar ve bu sınır kasıtlıdır ama yanlış okunması kolaydır. Her içerik akışı, her gömülü dosya, her çapraz referans akışı ve her nesne akışı taze bir 256 MiB ile başlar. 4.000 sayfalık bir belgenin bu yüzden tam tavanı harcamak için 4.000 bağımsız şansı vardır ve nesne akışları, her biri kendisi birçok nesne tutan sıkıştırılmış bir konteyner olduğu için sayıyı daha da çoğaltır, nesne akışları ve artımlı güncellemeler üzerine notlarda anlatıldığı gibi. Gerçek gereksiniminiz toplam süreç belleği üzerinde bir sınırsa, bu özellik onun tamamı değil bir girdisidir ve bir iş düzeyi veya konteyner düzeyi tavanın arkasında oturmalıdır
Sıfır sınırsız anlamına gelir ve bir kaçış kapağı değil meşru bir ayardır. Girdiye sahip olduğunuzda ayarlayın: kendi sisteminizin ürettiği belgeler üzerinde bir arşiv yeniden işleme boru hattı, veya tek bir 600 dpi renkli tarama zincirinin gerçekten rahat kodlayacağınız herhangi bir tavandan daha fazlasına ihtiyaç duyduğu bir rasterleştirme adımı. Negatif değerler, negatif bir bütçenin tutarlı bir anlamı olmadığı ve onu sessizce sınırlamanın bir yapılandırma hatasını gizleyeceği için önceden ERangeError ile reddedilir
// Trusted archive pipeline: state the intent rather than guess a ceiling
ArchivePdf.DecodeBudgetBytes := 0; // explicit unlimited
// Untrusted upload: size the ceiling from what your corpus actually needs
IngestPdf.DecodeBudgetBytes := 96 * 1024 * 1024;
// Configuration mistakes fail loudly instead of clamping
try
IngestPdf.DecodeBudgetBytes := -1;
except
on E: ERangeError do
LogWarning('DecodeBudgetBytes cannot be negative');
end;
Sayıyı seçmek genellikle aldığından daha fazla özen hak eder, çünkü çok düşük ayarlanmış bir bütçe kendi kendine verilen bir kesintidir. Mevcut korpusunuzu varsayılanla çalıştırın, her zincir için PeakStageBytes ve DecodedBytes'i kaydedin ve tavanı gözlemlenen maksimumun üzerinde, gerçek bir marj ile ayarlayın. Güvenli göründüğü için seçilen yuvarlak bir sayı, meşru büyük bir taramayı en kötü anda reddeder ve hata, günlüklerinizde tam olarak bir saldırı gibi görünür
Artık gerçekleşmeyen kopya
Her aşamayı bir bütçe sarmalayıcısı üzerinden yönlendirmek, zinciri daha pahalı yerine daha ucuz kılmayı başardı. Bir akışın filtreleri olduğunda, ilk aşama artık kodlanmış baytları önce bir taslak arabelleğe kopyalamak yerine kaynak akışı doğrudan okur ve buradan itibaren yalnızca iki arabellek aynı anda canlıdır: geçerli girdi ve yazılan aşama çıktısı. Ham kopya, buna ihtiyaç duyan iki durumda hayatta kalır, yani hiç filtresi olmayan bir akış ve çağıranın son kodlamanın korunmasını istediği bir görüntü, çünkü ikisi de çağıranın sahip olduğu ve bağımsız olarak arayabildiği bir akışı geri verir. Bu kodun korumasız versiyonu daha fazla tahsis ediyordu ve daha az sınırlıyordu, ki bu ikisi arasındaki olağan ilişkidir. Açıkça belirtmeye değer: bunların hiçbiri rastgele bir PDF'i yüklemeyi güvenli hâle getirmez. Küçük bir dosyanın iç içe geçmiş filtreler yoluyla büyük bir tahsis satın aldığı, belirli ve çok ucuz bir hizmet reddi vektörünü kapatır. Byte muhasebesindeki tamsayı taşması ayrı olarak korunur ve düşman belgeleri iç uzaklıklarına güvenmeden ayrıştırmanın daha geniş sorusu farklı bir disiplindir. Bir decode bütçesi birkaç sınırdan biridir ve değeri, dosyaya dokunmadan önce tek bir özellikten ayarlayabileceğiniz bir sınır olmasıdır
Zincir başına bütçe, tanı kaydı ve koruduğu yüklü-belge decode yolları, yapılandırılacak veya yamalanacak harici bir sıkıştırma açma bağımlılığı olmadan bileşenin kendisiyle birlikte gönderilir. Bir Delphi veya C++Builder hizmeti içinde güvenilmeyen PDF girdisini nasıl sınırlayacağınızı değerlendiriyorsanız, HotPDF Delphi PDF bileşeni sayfası, bu sınırların uygulandığı yüklü-belge araç setini listeler