Technical Article

Bezpieczne dla pamięci parsowanie PDF: Obrona przed złośliwymi dokumentami

Dokumenty PDF są niesamowicie potężne, ale ta wszechstronność niesie ze sobą nieodłączne ryzyko bezpieczeństwa. Ponieważ format PDF obsługuje pliki osadzone, interaktywny język JavaScript oraz złożone strumienie binarne, jest on często wykorzystywany jako wektor dystrybucji złośliwego oprogramowania. Przepełnienia bufora, odczyty poza zakresem pamięci (out-of-bounds) oraz przepełnienia liczb całkowitych w źle napisanych parserach PDF mogą prowadzić do zdalnego wykonania kodu (RCE).

Jeśli tworzysz w Delphi aplikację, która akceptuje przesłane przez użytkowników pliki PDF (np. portal do przyjmowania dokumentów), zapewnienie bezpiecznego dla pamięci parsowania plików PDF jest krytycznym wymogiem bezpieczeństwa.

Powszechne wektory ataków w PDF

Złośliwe pliki PDF zazwyczaj obierają za cel luki w samym parserze, a nie w systemie operacyjnym. Popularne techniki to między innymi:

  • Zniekształcone tabele odsyłaczy (XRef): Tworzenie takich przesunięć wskaźników (pointer offsets), które wykraczają poza zakres, co powoduje awarię parsera lub umożliwia ujawnienie zawartości pamięci.
  • Nieskończone pętle: Tworzenie odniesień cyklicznych między obiektami PDF (np. Obiekt A odwołuje się do Obiektu B, który z kolei odwołuje się do Obiektu A), co prowadzi do wyczerpania stosu.
  • Eksplodująca dekompresja (Zip Bombs): Strumienie FlateDecode, które z kilku kilobajtów dekompresują się do gigabajtów, wyczerpując pamięć systemu.

Strategie defensywnego parsowania w Delphi

Podczas natywnego parsowania plików PDF w Delphi musisz programować defensywnie. Nie możesz ufać metadanym zawartym w słownikach PDF.

1. Rozwiązywanie cyklicznych odniesień

Kiedy rekurencyjnie przechodzisz przez drzewo obiektów PDF, musisz zachować historię odwiedzonych już obiektów, by zapobiec pętlom nieskończonym.

uses
  System.Generics.Collections, System.SysUtils;

// A safe recursive function to walk the PDF tree
procedure ParsePDFDictionary(DictObj: TPDFDictionary; Visited: TList<Integer>);
var
  ObjID: Integer;
begin
  ObjID := DictObj.ObjectID;
  
  if Visited.Contains(ObjID) then
  begin
    Writeln('Warning: Circular reference detected. Aborting branch.');
    Exit;
  end;
  
  Visited.Add(ObjID);
  
  try
    // Process child objects safely...
  finally
    // Allow siblings to traverse, but prevent vertical recursion loops
    Visited.Remove(ObjID);
  end;
end;

2. Ochrona przed bombami dekompresyjnymi (Zip Bombs)

Podczas korzystania z filtra FlateDecode do dekompresji strumienia, musisz bezwzględnie ograniczyć jego maksymalny dopuszczalny rozmiar rozszerzenia. Nigdy nie przydzielaj pamięci w ciemno, opierając się wyłącznie na kluczu słownikowym /Length.

const
  MAX_DECOMPRESSED_SIZE = 1024 * 1024 * 50; // 50 MB safety limit

procedure DecompressPDFStream(CompressedStream, OutputTarget: TStream);
var
  ZLibStream: TZDecompressionStream;
  Buffer: array[0..8191] of Byte;
  BytesRead, TotalRead: Integer;
begin
  ZLibStream := TZDecompressionStream.Create(CompressedStream);
  try
    TotalRead := 0;
    repeat
      BytesRead := ZLibStream.Read(Buffer[0], SizeOf(Buffer));
      if BytesRead > 0 then
      begin
        TotalRead := TotalRead + BytesRead;
        if TotalRead > MAX_DECOMPRESSED_SIZE then
          raise Exception.Create('Security Exception: Decompression bomb detected!');
          
        OutputTarget.WriteBuffer(Buffer[0], BytesRead);
      end;
    until BytesRead = 0;
  finally
    ZLibStream.Free;
  end;
end;

Wykorzystanie wzmocnionych silników i bezpiecznych komponentów

Napisanie całkowicie bezpiecznego parsera PDF od podstaw to monumentalne zadanie. Standardowym podejściem w branży jest użycie wzmocnionego, intensywnie testowanego za pomocą fuzzingu silnika, takiego jak PDFium, lub poleganie na rygorystycznie przetestowanych natywnych bibliotekach.

PDFium to główny silnik renderujący używany przez Google Chrome. Ponieważ Chrome przetwarza codziennie miliony niezaufanych plików PDF, PDFium jest poddawane agresywnemu, ciągłemu fuzzingowi przez Project Zero firmy Google. Płynnie obsługuje zniekształcone tabele XRef, uszkodzone strumienie i odniesienia cykliczne.

Podobnie, natywne komponenty, takie jak HotPDF Component i Delphi PDF Library, mają wbudowane solidne, defensywne strategie parsowania. Implementują one rygorystyczne sprawdzanie granic, ograniczniki głębokości rekurencji i mechanizmy zapobiegania wyciekom pamięci zaprojektowane specjalnie dla środowisk Delphi i C++Builder.

Niezależnie od tego, czy zdecydujesz się na korzystanie z PDFium za pośrednictwem wrappera Delphi do renderowania, czy na wykorzystanie natywnych komponentów, takich jak HotPDF, do generowania i przetwarzania dokumentów, dziedziczysz obwód bezpieczeństwa klasy korporacyjnej, chroniący użytkowników i serwery przed złośliwymi ładunkami, bez konieczności samodzielnego pisania defensywnych parserów.

Uwaga: Bezpieczne, testowane za pomocą fuzzingu możliwości parsowania są dostępne w całym naszym pakiecie, obejmującym HotPDF Component, Delphi PDF Library i PDFium Component.