Plik PDF to nie tylko wirtualny papier. To kontener, który może przenosić skrypty uruchamiane przy otwarciu pliku, linki otwierające programy zewnętrzne, odnośniki łączące się z serwerami WWW, pliki zagnieżdżone w innych plikach oraz podpis poświadczający, że dokument nie został zmodyfikowany od momentu jego certyfikacji. Gdy plik pochodzi z kontrolowanego przez kogoś innego źródła, najbezpieczniejszym pierwszym krokiem nie jest jego wyrenderowanie. Jest nim odczytanie tego, co plik mówi o sobie, i zbudowanie wykazu wszystkiego, co próbowałby zrobić, tak aby człowiek mógł zdecydować, czy w ogóle pasuje do realizowanego procesu pracy.
Ten artykuł opisuje statyczny, przeznaczony tylko do odczytu proces audytu obszaru ryzyka przy użyciu komponentu PDFium dla środowisk Delphi i Lazarus. Audyt ten nigdy nie rysuje strony. Analizuje on strukturę dokumentu, wykazuje części pliku niosące określone zachowania i generuje prosty raport. To różnica między poproszeniem nieznajomego o opróżnienie kieszeni przy wejściu a zaufaniem mu tylko dlatego, że ładnie się uśmiechnął.
Czym jest audyt, a czym nie jest
Miejmy jasność co do granic działania. Podgląd w piaskownicy (sandboxed preview) renderuje plik przy zachowaniu surowych ograniczeń, pozwalając użytkownikowi na wgląd do dokumentu bez wchodzenia pliku w interakcję z resztą komputera. Audyt ma miejsce przed tym etapem. Jest to analiza bez renderowania, której jedynym wynikiem jest opis obszaru zagrożeń: jakie skrypty istnieją, jakie akcje są przypisane do linków, czy plik jest podpisany i jak silnie oraz jakie zawiera załączniki. Uruchamiasz go, gdy dokument przekracza granicę zaufania — przy pobieraniu z wiadomości e-mail, formularza przesyłania czy zewnętrznego źródła danych — zanim kolejny etap otworzy go na stałe.
Komponent ładuje dokument do celów audytu w ten sam sposób co w innych przypadkach. Ustawiasz nazwę pliku i go aktywujesz, co parsuje tablicę powiązań (cross-reference) i katalog dokumentu bez renderowania chociażby jednej strony. Wszystkie poniższe kroki odczytują dane z tego załadowanego, niewyrenderowanego stanu.
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Incoming_Invoice.pdf';
Pdf.Active := True; // parses structure, renders nothing
// audit the loaded document here
finally
Pdf.Free;
end;
end;
JavaScript na poziomie dokumentu w drzewie nazw
Pierwszą rzeczą do wykazania jest kod. Dokument PDF może przenosić JavaScript na poziomie dokumentu — skrypty nieprzypisane do żadnej strony ani pola, lecz do samego dokumentu, zapisane w drzewie /Names we wpisie /JavaScript. Zgodna z normami przeglądarka uruchamia te skrypty przy otwarciu. Jest to mechanizm leżący u podstaw wielu złośliwych plików PDF, ponieważ pozwala na wykonanie kodu w momencie kliknięcia pliku przez użytkownika, zanim ten w ogóle przeczyta choćby jedno słowo.
Audytor potrzebuje dwóch faktów na temat każdego takiego skryptu: informacji o jego istnieniu oraz o jego zawartości. Komponent udostępnia liczbę skryptów i pozwala odczytać każdą akcję jako rekord zawierający nazwę skryptu oraz jego pełną treść. Reading the body matters. A script named Doc.0 tells you nothing, but its text might call app.launchURL or assemble a string and pass it somewhere it should not go. Pulling the source out so a reviewer can read it is the whole point of flagging a file that runs code on open.
var
I: Integer;
Action: TPdfJavaScriptAction;
begin
if Pdf.JavaScriptActionCount > 0 then
WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
' script(s) on open');
for I := 0 to Pdf.JavaScriptActionCount - 1 do
begin
Action := Pdf.JavaScriptAction[I];
WriteLn(' script "', Action.Name, '":');
WriteLn(Action.Script); // full body, for a human to read
end;
end;
Plik bez skryptów dokumentu nie jest automatycznie bezpieczny, ponieważ istnieją również skrypty stron i pól, ale plik ze skryptami dokumentu zawsze wymaga dodatkowej analizy. Sama obecność i liczba skryptów stanowią ważną bramkę kontrolną, a treść kodu pozwala na podjęcie ostatecznej decyzji.
Akcje Launch i URI
Kolejne zachowania wymagające inwentaryzacji znajdują się w odnośnikach i adnotacjach. Dla audytora najważniejsze są dwa typy akcji. Akcja Launch (uruchomienie) włącza zewnętrzny program lub otwiera lokalny plik w momencie aktywacji linku. Akcja URI otwiera cel w sieci Web. Recenzent sprawdzający podejrzany dokument powinien widzieć, bez klikania w cokolwiek, że przycisk na stronie trzeciej ma przypisaną komendę uruchomienia cmd.exe lub otwiera adres URL niezgodny z marką prezentowaną w dokumencie.
Komponent klasyfikuje znalezione odnośniki i udostępnia typ akcji oraz ścieżkę docelową dla każdego z nich, co pozwala audytowi na wylistowanie każdej akcji Launch i URI wraz z jej celem. To jest faza raportowania, a nie wykonywania kodu. Audytor odczytuje akcję ze struktury dokumentu i ją zapisuje. Nigdy za nią nie podąża.
Kontrolka przeglądarki renderująca dokumenty to miejsce, w którym mogłoby nastąpić wykonanie akcji, a jej domyślna konfiguracja jest celowo ostrożna. Kontrolka TPdfView posiada zbiór LinkOptions, który decyduje, jakie typy linków są automatycznie uruchamiane po kliknięciu. Wartością domyślną jest [loAutoGoto, loAutoOpenURI], co oznacza, że skoki wewnątrz dokumentu i adresy URL mogą się otwierać, ale parametr loAutoLaunch jest nieobecny, więc akcje uruchomienia programów zewnętrznych nigdy nie włączają się automatycznie. W procesie audytu idzie się krok dalej i czyści ten zbiór całkowicie, tak aby nic nie uruchamiało się automatycznie podczas oceny zaufania do pliku.
// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];
// The shipped default already withholds launch:
// default = [loAutoGoto, loAutoOpenURI]
// loAutoLaunch is NOT in the default set, so external programs
// are never started on a stray click out of the box.
Uzasadnienie zablokowania akcji uruchamiania (launch) jest proste. Skok w obrębie dokumentu jest nieszkodliwy, a adres URL jest widoczny i można go zamknąć, ale włączenie zewnętrznego programu po kliknięciu to najgroźniejsza rzecz, jakiej może zażądać link PDF, dlatego funkcja ta pozostaje wyłączona bez wyraźnej zgody. Audytor wyłącza nawet bezpieczne zachowania, ponieważ jego zadaniem jest analiza, a nie interakcja.
Poziom uprawnień MDP podpisu cyfrowego
Podpisy cyfrowe zmieniają postać rzeczy. Zwykły podpis poświadcza stan bajtów w momencie podpisywania. Podpis certyfikujący — tworzony z regułą wykrywania i zapobiegania modyfikacjom dokumentu (MDP) — idzie dalej: określa, co może się legalnie zmienić po certyfikacji dokumentu, a zgodna przeglądarka ostrzega, jeśli zmodyfikowano cokolwiek spoza tego zakresu. Odczytanie poziomu uprawnień informuje audytora, czy plik jest certyfikowany, a jeśli tak, to jak silnie powinien być zabezpieczony.
Uprawnienie MDP to liczba całkowita o trzech zdefiniowanych wartościach. Poziom 1 oznacza całkowity brak możliwości wprowadzania zmian; dowolna modyfikacja narusza certyfikację. Poziom 2 zezwala na wypełnianie formularzy i podpisywanie — typowy przypadek dla umów, które mają zostać wypełnione i podpisane, ale nie zmieniane w żaden inny sposób. Poziom 3 dodatkowo dopuszcza dodawanie adnotacji obok wypełniania formularzy i podpisywania. Znajomość poziomu pozwala logice weryfikacyjnej na ocenę intencji: dokument certyfikowany na poziomie 1, który mimo to zawiera aktywne pola formularza lub skrypty, przeczy sam sobie i ta sprzeczność jest warta zasygnalizowania.
Komponent odczytuje liczbę podpisów i prezentuje każdy z nich jako rekord, którego pole Permission zawiera tę wartość MDP, pobraną bezpośrednio z wywołania niskopoziomowego FPDFSignatureObj_GetDocMDPPermission. Uprawnienie o wartości zero oznacza, że podpis nie jest podpisem certyfikującym (DocMDP), więc nie ma do zaraportowania blokady na poziomie dokumentu.
var
I: Integer;
Sig: TPdfSignature;
begin
if Pdf.SignatureCount = 0 then
WriteLn('document is not signed')
else
for I := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[I];
case Sig.Permission of
1: WriteLn('certified: no changes allowed');
2: WriteLn('certified: form fill and signing allowed');
3: WriteLn('certified: form fill, signing and annotations allowed');
else
WriteLn('signed, but not a DocMDP certification');
end;
end;
end;
Audyt nie weryfikuje tutaj kryptograficznej poprawności podpisu; badanie łańcucha certyfikatów to osobne zadanie. Raportuje natomiast zadeklarowaną intencję: informację, że ten plik został zablokowany na tym poziomie. To jest dokładnie ten kontekst, którego recenzent potrzebuje do oceny, czy późniejsze zmiany lub sama obecność aktywnej zawartości są spójne ze sposobem zabezpieczenia dokumentu przez autora.
Pozostały obszar: osadzone pliki i XFA
Dwa kolejne elementy uzupełniają pełną inwentaryzację. Osadzone pliki (embedded files) to całe dokumenty umieszczone wewnątrz PDF jako załączniki i stanowią one klasyczny wektor ataków, ponieważ niewinnie wyglądający raport może przenosić plik wykonywalny lub drugi złośliwy dokument PDF w strukturze załączników. Komponent udostępnia liczbę załączników oraz nazwę każdego z nich, dzięki czemu audyt może spisać to, co jest dołączone do dokumentu, bez rozpakowywania ani otwierania żadnego z tych elementów.
Inną flagą jest obecność XFA. Formularz XFA zastępuje statyczny formularz AcroForm strukturą opartą na XML, która wnosi własny model renderowania i skryptów — znacznie szerszy i bardziej skomplikowany obszar działania niż zwykły formularz. Nie musisz przetwarzać XFA, aby odnotować jego obecność; sam ten fakt to sygnał, że plik zawiera bogatszą warstwę interaktywną wymagającą uwagi. Komponent zgłasza to jako pojedynczą wartość logiczną.
var
I: Integer;
begin
if Pdf.XFA then
WriteLn('NOTE: document contains an XFA form layer');
if Pdf.AttachmentCount > 0 then
begin
WriteLn('embedded files: ', Pdf.AttachmentCount);
for I := 0 to Pdf.AttachmentCount - 1 do
WriteLn(' - ', Pdf.AttachmentName[I]);
end;
end;
Jedna procedura tylko do odczytu generująca raport
Łącząc te elementy, audyt sprowadza się do pojedynczej procedury, która ładuje dokument, wykazuje jego skrypty wraz z ich treścią, spisuje cele akcji Launch i URI, zgłasza poziom MDP podpisów, odnotowuje obecność załączników i XFA oraz zapisuje wyniki w dzienniku. Procedura ta niczego nie renderuje, dzięki czemu jest wydajna i nie da się jej oszukać w celu wyświetlenia wrogiej zawartości strony. Wynik to płaski, czytelny dla człowieka rekord, na podstawie którego recenzent lub reguła przetwarzania może podjąć dalsze decyzje.
Rozwiązaniem sprawdzającym się w praktyce jest zbieranie każdego znaleziska w osobnym wierszu, oznaczenie tych rzeczywiście ryzykownych prefiksem w celu przesunięcia ich na górę kolejki przeglądu i zapisanie całości obok analizowanego pliku. Dokument bez skryptów, bez akcji uruchomienia, bez załączników, bez XFA i bez podpisów (lub ze spójną certyfikacją) przechodzi weryfikację bez echa. Dokument aktywujący kilka flag jednocześnie to ten, który człowiek powinien sprawdzić, zanim zostanie otwarty w jakimkolwiek innym systemie. Audytor nie podejmuje decyzji o zaufaniu za Ciebie. Zapewnia jedynie, że decyzja ta opiera się na wiedzy, a nie na ślepym zaufaniu.
Gdy plik przejdzie pomyślnie audyt i musisz go wyświetlić, zrób to przy zachowaniu ograniczeń, a nie w domyślnej przeglądarce. Podejście opisane w naszym poradniku na temat budowy bezpiecznego podglądu PDF w Delphi pokazuje, jak zablokować automatyczną obsługę linków i aktywną zawartość podczas kontrolowanego przeglądania. Aby włączyć tę inwentaryzację do pełnego procesu weryfikacji z narzędziami dla recenzentów, przeczytaj artykuł warsztat weryfikacji i pobierania PDF. Obie te metody bazują na tych samych fundamentach tylko do odczytu, bez renderowania zawartości, i wchodzą w skład pakietu PDFium Component dla Delphi i C++Builder obok interfejsów API do renderowania, tekstu, formularzy i podpisów, o których mowa w innych artykułach na tym blogu.