Artykuł techniczny

Bezpieczny podgląd PDF w Delphi z PDFium Component

Podgląd niezaufanego PDF wewnątrz własnej aplikacji to decyzja o wykonaniu kodu, a tym, co się liczy, nie jest oprawa przeglądarki, lecz to, czego panel z własnej woli odmawia. Nie zapisuj pliku na dysk. Nie pozwól jego linkom wychodzić do powłoki. Nie podawaj jego załącznikom ścieżki. Większość szkód wyrządzanych przez wrogi dokument bierze się nie z podatności silnika, lecz z tego, że przeglądarka robi całkowicie zwyczajne rzeczy z wejściem dostarczonym przez atakującego: otwiera link file:// do udziału UNC, który wycieka poświadczenia NTLM, zostawia kopię roboczą w katalogu tymczasowym, kopiuje osadzone ładunki tam, dokąd każe jej ciąg z nazwą pliku. PDFium Component to przeglądarka PDF z kodem źródłowym dla Delphi, C++Builder i Lazarusa, która umieszcza istotne przełączniki tam, gdzie możesz po nie sięgnąć: flaga w czasie wczytania ubijająca skrypty, zdarzenia kliknięcia linku, które możesz zawetować, dostęp do załączników biegnący przez twój własny kod i bity uprawnień, które możesz odczytać. Poniższa kolejność podąża za dokumentem od chwili, gdy przybywa, do chwili, gdy użytkownik coś w nim klika

Model zagrożeń panelu podglądu

Bądź uczciwy co do tego, co daje ci „bezpieczny podgląd”. Renderer parsuje niezaufane bajty niezależnie od tego, co zrobisz, a własne utwardzenie silnika to podłoga, na której stoisz. Wszystko powyżej tej podłogi to polityka aplikacji: czy skrypty się inicjalizują, co robi kliknięcie linku, czy pliki osadzone mogą sięgnąć dysku, czy schowek i drukarka są drzwiami, czy ścianą. Jedną rzecz warto od razu spisać na straty: silnikowy przełącznik FPDF_SetSandBoxPolicy. Większość ograniczeń silnika jest wkompilowana, przełącznik zmienia w praktyce niewiele, a przypisanie mu jakiejkolwiek roli w twojej historii izolacji produkuje tylko fałszywe poczucie zrobienia czegoś. Gdy wejście jest naprawdę wrogie, powiedzmy przy publicznym portalu wysyłania plików, jedyną prawdziwą izolacją jest renderowanie w osobnym procesie o niskich uprawnieniach i wysyłanie bitmap do interfejsu. Flagi w procesie to polityka. Nie są ograniczeniem

Diagram PDFium Component zestawiający przełączniki polityki podglądu PDF w procesie z renderowaniem poza procesem w robotniku o niskich uprawnieniach dla wrogich dokumentów
Flagi w procesie podnoszą poprzeczkę dla znanych nadawców, a anonimowe wysyłki uzasadniają osobnego robotnika o niskich uprawnieniach, który wysyła do interfejsu wyłącznie bitmapy

O dwóch powierzchniach łatwo zapomnieć właśnie dlatego, że żadne kliknięcie ich nie dotyka. Pierwszą są pliki tymczasowe. Jeśli twój potok odkłada przychodzące dokumenty na dysk przed podglądem, te odłożone kopie przeżywają sesję, chyba że coś je sprawdzalnie usunie, a plik „do odzyskania z katalogu tymczasowego” po cichu pokonał każdą kontrolę, którą egzekwuje sam panel. Wczytuj zamiast tego z pamięci przez TPdfStreamAdapter, żeby wrogie bajty nigdy nie dostały własnej ścieżki. Drugą jest schowek. Podgląd, który pozwala zaznaczyć i skopiować, już wyeksportował dokument, po jednym ekranie naraz, i żadne przechwytywanie linków tego nie złapie

Ubij JavaScript w czasie wczytania, a nie w interfejsie

JavaScript dokumentu w PDFium Component inicjalizuje się wyłącznie razem ze środowiskiem wypełniania formularzy. Wczytanie z FormFill := False wyłącza więc skrypty u korzenia, zamiast tłumić ich objawy:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // brak środowiska formularzy, więc brak silnika JavaScript
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // surowe słowo flag; wszystkie bity = bez ograniczeń
end;

Kompromis jest realny i należy do twojej specyfikacji. Przy wyłączonym wypełnianiu formularzy znika też legalna interakcja z AcroForm i skrypty walidujące; pola renderują się z ostatnim zapisanym wyglądem, ale nie da się ich edytować. Dla panelu podglądu jest to zwykle właściwy wybór, bo podgląd oznacza patrzenie, a nie wypełnianie. Ale jeśli to samo okno służy zarazem jako powierzchnia do wypełniania zaufanych dokumentów wewnętrznych, odpowiedzią są dwie ścieżki wczytania z jawną decyzją o zaufaniu pomiędzy nimi, a nie jedna ścieżka z ustawieniem kompromisowym, zbyt luźnym dla przypadku wrogiego i zbyt ciasnym dla zaufanego. Strona formularzowa tego podziału ma własne pułapki, omówione w artykule o nawigacji po polach formularza i regeneracji wyglądów

Linki: domyślna obsługa wychodzi do powłoki

Zostawione samym sobie kliknięcia linków idą prosto do systemu operacyjnego. Domyślne LinkOptions przeglądarki zawierają loAutoOpenURI, czyli czekający na swoją chwilę wyciek z file:// do udziału UNC. Punkt przewężenia tworzą dwa zdarzenia: OnWebLinkClick dla adresów URL wykrytych w tekście strony i OnAnnotationLinkClick dla adnotacji linków niosących akcje URI albo launch. Ustaw Handled := True w obu, bezwarunkowo, zanim cokolwiek zdecydujesz, a potem ponownie dopuść wyłącznie to, na co pozwala polityka. Jako drugą warstwę usuń loAutoOpenURI z LinkOptions dla wrogiego wejścia i upewnij się, że loAutoLaunch, domyślnie wyłączone, nigdy nie wraca tylnymi drzwiami przez skopiowaną konfigurację:

Diagram przepływu przechwytywania kliknięć linków PDF w panelu podglądu w Delphi z kontrolą przedrostka schematu na surowym ciągu i logowaniem audytowym zablokowanych linków
Ustawienie Handled w obu zdarzeniach linków trzyma każde kliknięcie pod polityką aplikacji, a kontrola przedrostka na surowym ciągu trzyma poza nią schematy file:// i UNC
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // nigdy nie przepuszczaj do domyślnego zachowania powłoki

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

O tym, czy to naprawdę się utrzyma, decydują dwa szczegóły. Po pierwsze, kontrola schematu musi być kontrolą przedrostka na surowym ciągu, przed jakimkolwiek parsowaniem, bo file://, ścieżki UNC i egzotyczne schematy to dokładnie te wartości, które wywalają naiwny parser adresów URL albo prześlizgują się przez taki, który normalizuje zbyt gorliwie. Po drugie, loguj każdą blokadę z dołączoną tożsamością dokumentu. Garść zablokowanych linków file:// to szum tła; ich seria w wielu przychodzących dokumentach w krótkim oknie czasu to incydent, o którym twój zespół bezpieczeństwa wolałby usłyszeć od ciebie niż skądinąd

Załączniki: polityka rozszerzeń i nazwa pliku, której nie wybrałeś

PDF jest kontenerem, a AttachmentCount wraz z właściwością AttachmentName[] mówi ci, co niesie, zanim cokolwiek dotknie dysku. Liczą się tu dwie osobne kontrole i tylko jedna z nich jest oczywista. Oczywista to polityka typów: lista dozwolonych rozszerzeń, które w ogóle wolno wyeksportować. Subtelna jest taka, że nazwa załącznika to dane kontrolowane przez atakującego, kropka. Osadzona nazwa w rodzaju ..\..\Startup\update.exe zamienia niedbały zapis w przejście po ścieżce, które upuszcza plik wykonywalny do folderu uruchamianego przez Windows przy logowaniu. Komponent podaje ci ładunek jako bajty przez Attachment[] i pozwala twojemu kodowi wybrać ścieżkę, więc buduj tę ścieżkę z odkażonej nazwy bazowej, a nigdy z surowego ciągu osadzonego:

Diagram potoku PDFium Component odkażającego kontrolowaną przez atakującego nazwę załącznika PDF przez ExtractFileName i listę dozwolonych rozszerzeń przed zapisem bajtów
Osadzona nazwa załącznika to wejście atakującego, więc ścieżkę eksportu odbudowuje się z odkażonej nazwy bazowej i bramkuje domyślnie odmawiającą listą dozwolonych rozszerzeń
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // usuwa wszelkie składniki ścieżki
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // lista dozwolonych, nie zabronionych
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // osadzony ładunek jako surowe bajty
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Wybieraj kierunek listy dozwolonych. Lista zabronionych „niebezpiecznych” rozszerzeń to wyścig, który przegrywasz w dniu, gdy ktoś uzbroi rozszerzenie, o jakim nigdy nie słyszałeś; lista dozwolonych z .pdf, .png i .csv zawodzi w stronę odmowy

Co naprawdę obiecują uprawnienia szyfrowania

Standardowy handler bezpieczeństwa z ISO 32000-1 koduje flagi uprawnień do drukowania, kopiowania treści i modyfikacji, a właściwości Permissions i UserPermissions wystawiają je jako surowe maski bitowe, gdy dokument się otworzy. Tabela 22 w ISO 32000-1 definiuje te bity, a plik niezaszyfrowany raportuje wszystkie bity ustawione. Odczytuj je i honoruj w swojej warstwie poleceń, ale miej jasność, czym są. Dla dokumentu zaszyfrowanego hasłem właściciela i pustym hasłem użytkownika treść odszyfrowuje się w pełni przy otwarciu, a flagi są prośbą do zgodnych przeglądarek, nie mechanizmem egzekwowania. Ma to dwie konsekwencje ciągnące w przeciwnych kierunkach. Nigdy nie przedstawiaj użytkownikom flag uprawnień jako właściwości bezpieczeństwa dokumentów, które otrzymują, bo nią nie są. Zarazem honoruj bit wydobywania na potrzeby dostępności (bit 10) nawet tam, gdzie ogólne kopiowanie (bit 5) jest odmówione; dostęp dla czytników ekranu jest w modelu uprawnień celowo wydzielony osobno, a odbieranie go, bo „kopiowanie jest wyłączone”, psuje technologie wspomagające bez żadnego zysku dla bezpieczeństwa

Egzekwuj odmówione akcje na poziomie poleceń, a nie przez ukrywanie przycisków paska narzędzi. Ctrl+C, menu kontekstowe i zaznaczanie przeciągnięciem omijają pasek narzędzi; pojedyncza kontrola uprawnień wewnątrz polecenia kopiowania nie omija niczego

Dla dokumentów, które faktycznie wymagają hasła użytkownika, przypisz Password przed Active := True i traktuj tę wartość jak sekret, którym jest: pobieraj ją z magazynu poświadczeń na każdą sesję, trzymaj poza logami i raportami awarii i nigdy nie utrwalaj obok dokumentu. Panel podglądu, który buforuje hasła „dla wygody”, po cichu stał się bazą haseł bez żadnej z ochron, jakie ta powinna mieć

Drukowanie zasługuje na własną decyzję, a nie na odziedziczenie tego, co wypadło z reguły kopiowania. Wydruk fizyczny jest z definicji nieaudytowalny, ale blokowanie druku na sztywno zwykle pcha użytkowników w stronę zrzutów ekranu, które są gorsze pod każdym względem. Częstym środkiem pośrednim jest dopuszczenie druku ze stemplowaniem każdej strony tożsamością użytkownika i znacznikiem czasu, egzekwowanym wewnątrz polecenia drukowania. Miej tylko wobec tego właściwe oczekiwania: znak wodny to odstraszanie i przypisanie autorstwa. Nie jest zapobieganiem

Co przyjmowanie powinno ci już powiedzieć

Panel podglądu podejmuje lepsze decyzje, gdy plik przychodzi z dołączoną teczką: zaszyfrowany czy nie, JavaScript obecny czy nie, spis załączników, typ formularza. Ten przebieg inspekcyjny należy przed przeglądarkę, a wzorzec z artykułu o budowaniu warsztatu przyjmowania i przeglądu PDF produkuje dokładnie te flagi, które chce konsumować polityka podglądu. Pliki oznaczone przy przyjmowaniu jako ryzykowne otwierają się automatycznie ścieżką utwardzoną; rutynowe dokumenty zachowują swoje wygody. Zwiąż oba etapy z jednym wspólnym obiektem polityki, a nie z dwoma ekranami konfiguracji, które rozjadą się do drugiego wydania, choćbyś nie wiem jak starannie pisał je za pierwszym razem

To, gdzie pada linia między w procesie a poza procesem, zależy od tego, kto przysyła ci pliki. W zwykłym przyjmowaniu biznesowym ludzie przysyłający dokumenty są znani i najwyżej niedbali, a podgląd w procesie z wyłączonymi skryptami i przechwyconymi linkami to poprzeczka, której da się bronić. Dla anonimowych wysyłek publicznych już nie, i żadna ilość ustawiania flag w procesie tego nie zmieni; renderuj te pliki w osobnym robotniku o niskich uprawnieniach i wysyłaj bitmapy do interfejsu, żeby wada silnika kosztowała cię robotnika, a nie aplikację hosta. Wybierz ten podział świadomie i zapisz, do którego koszyka wpada każda ścieżka przyjmowania, bo koszt złego zgadnięcia jest asymetryczny

Licencjonowanie, powierzchnia API związana z bezpieczeństwem i demonstracja utwardzonej przeglądarki są na stronie produktu: PDFium Component