Artykuł techniczny

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

Dokumenty PDF oferują ogromne możliwości, a ta elastyczność niesie ze sobą nieodłączne ryzyko bezpieczeństwa. Ponieważ PDF obsługuje osadzone pliki, interaktywny JavaScript i złożone strumienie binarne, jest często wykorzystywany jako wektor dystrybucji złośliwego oprogramowania. Przepełnienia bufora, odczyty spoza zakresu pamięci i przepełnienia liczb całkowitych w źle napisanych parserach PDF mogą prowadzić do zdalnego wykonania kodu (RCE)

Jeśli tworzysz w Delphi aplikację przyjmującą pliki PDF przesyłane przez użytkowników (np. portal do przyjmowania dokumentów), bezpieczne dla pamięci parsowanie PDF jest krytycznym wymaganiem bezpieczeństwa

Typowe wektory ataków w plikach PDF

Złośliwe pliki PDF zazwyczaj atakują luki w samym parserze, a nie w systemie operacyjnym. Typowe techniki to:

  • Zniekształcone tabele krzyżowych odniesień (XRef): Tworzenie przesunięć wskaźników wykraczających poza zakres, powodujące awarię parsera lub ujawnienie zawartości pamięci
  • Nieskończone pętle: Tworzenie cyklicznych odniesień między obiektami PDF (np. obiekt A odwołuje się do obiektu B, który odwołuje się do obiektu A), prowadzące do wyczerpania stosu
  • Eksplodująca dekompresja (Zip Bomb): Strumienie FlateDecode, które rozpakowują się z kilku kilobajtów do gigabajtów, wyczerpując pamięć systemową

Strategie defensywnego parsowania w Delphi

Parsując pliki PDF natywnie w Delphi, musisz programować defensywnie. Nie możesz ufać metadanym zawartym w słownikach PDF

1. Wykrywanie cyklicznych odniesień

Podczas rekurencyjnego przechodzenia przez drzewo obiektów PDF musisz prowadzić historię odwiedzonych obiektów, aby zapobiec nieskończonym pętlom

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 Zip Bomb

Stosując filtr FlateDecode do dekompresji strumienia, musisz bezwzględnie ograniczyć maksymalny rozmiar danych wyjściowych. Nigdy nie przydzielaj pamięci ślepo na podstawie klucza słownika /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;

Korzystanie z wzmocnionych silników i bezpiecznych komponentów

Napisanie od podstaw całkowicie bezpiecznego parsera PDF to ogromne przedsięwzięcie. Branżowym standardem jest korzystanie z wzmocnionego, intensywnie testowanego za pomocą fuzz testów silnika, takiego jak PDFium, lub poleganie na rygorystycznie przetestowanych bibliotekach natywnych

PDFium to podstawowy silnik renderowania używany przez Google Chrome. Ponieważ Chrome przetwarza miliony niezaufanych plików PDF każdego dnia, PDFium jest poddawany agresywnemu, ciągłemu fuzz testingowi przez OSS-Fuzz i ClusterFuzz. Obsługuje zniekształcone tabele XRef, uszkodzone strumienie i odwołania cykliczne bez awarii

Podobnie natywne komponenty takie jak HotPDF Component i Delphi PDF Library zawierają dobrze przetestowane strategie defensywnego parsowania od razu po wyjęciu z pudełka. 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ę korzystać z PDFium za pośrednictwem wrappera Delphi do renderowania, czy używać natywnych komponentów, takich jak HotPDF, do generowania i przetwarzania dokumentów - zyskujesz korporacyjny poziom bezpieczeństwa, który chroni Twoich użytkowników i serwery przed złośliwymi ładunkami bez konieczności samodzielnego pisania defensywnych parserów

Uwaga: Bezpieczne, przetestowane za pomocą fuzz testów możliwości parsowania są dostępne w całym naszym pakiecie, w tym w HotPDF Component, Delphi PDF Library i PDFium Component

Limity dekompresji i kontrola grafu obiektów

Strumień FlateDecode może wielokrotnie powiększyć dane wejściowe, dlatego limit należy sprawdzać w pętli dekompresji, a także utrzymywać budżet dla całego dokumentu. PDF jest grafem, nie drzewem: licznik głębokości i zbiór odwiedzonych obiektów wspólnie wykrywają zarówno nadmierne zagnieżdżenie, jak i cykle

Lista kontroli przed wdrożeniem

Przed wdrożeniem sprawdź przycięte długości strumieni, parametry dekoderów obrazu, obliczenia w Int64, aktywne {$R+}, granice każdego odczytu, limity głębokości oraz budżety dekompresji. Każdy limit powinien kończyć się rejestrowanym odrzuceniem, a nie uszkodzeniem pamięci