Warsztat przeglądu przyjmowanych plików PDF to niewielki program z jednym zadaniem: przyjrzeć się każdemu plikowi, zanim cokolwiek dalej w łańcuchu dostanie pozwolenie, by go dotknąć. Aby wykonać to zadanie, musi on złożyć kilka możliwości w jednym przebiegu. Otwiera plik (bez okazywania mu zaufania), odczytuje to, co plik deklaruje o samym sobie, szuka treści, które wprowadzą w błąd naiwny ekstraktor albo przeniosą atak, rozstrzyga, czy w ogóle istnieje tekst do wyodrębnienia, a następnie kieruje dokument do kolejki na podstawie tego, co znalazł. Pominięcie inspekcji kończy się cichymi awariami: PDF zaszyfrowany hasłem właściciela i opakowujący formularz XFA przechodzi przez ekstraktor tekstu jako puste ciągi, zostaje zaindeksowany jako pusty dokument i nikt niczego nie zauważa, dopóki ktoś dalej w łańcuchu nie zacznie szukać treści, której nigdy nie odczytano. Komponent PDFium to biblioteka VCL/LCL do podglądu i inspekcji dostępna w kodzie źródłowym dla Delphi, C++Buildera i Lazarusa i udostępnia ona wywołania introspekcyjne, których ten warsztat potrzebuje. Poniższe sekcje prowadzą przez to, które wywołanie odpowiada na które pytanie, oraz przez dwa miejsca, w których oczywiste wywołanie daje odpowiedź błędną, choć pewną siebie
Pięć pytań, na które trzeba odpowiedzieć przed skierowaniem pliku
Odrzuć siatkę danych i pasek miniatur, a triaż przyjmowanych plików sprowadza się do pięciu pytań:
- Czy plik w ogóle da się otworzyć i pod jakim hasłem?
- Co deklaruje, że jest: tytuł, autor, data utworzenia?
- Czy zawiera treść aktywną lub ryzykowną, taką jak JavaScript, formularz XFA albo osadzone pliki?
- Czy jest tekst do wyodrębnienia, czy to skan zmierzający do OCR?
- Biorąc to wszystko pod uwagę, do której kolejki trafia: przetwarzanie bezpośrednie, przegląd ręczny czy kwarantanna?
Każde pytanie przekłada się na jedno lub dwa wywołania komponentu PDFium. Dwa z tych odniesień mają ostre krawędzie, które odpowiadają za większość błędnie skierowanych plików, jakie zdarzyło mi się debugować na produkcji. Metadane dokumentu żyją w dwóch różnych miejscach, które mogą się ze sobą rozjeżdżać, a szyfrowanie niekoniecznie powstrzymuje dokument przed otwieraniem
Otwieraj tanio: bez wypełniania formularzy, zero renderowanych stron
Triaż powinien być najtańszym możliwym otwarciem. Ustawienie FormFill := False przed Active := True nakazuje komponentowi całkowicie pominąć środowisko wypełniania formularzy. Skraca to czas ładowania i (co równie istotne dla plików nieznanego pochodzenia) zapobiega inicjalizacji jakiegokolwiek JavaScriptu na poziomie dokumentu. Żadna z właściwości inspekcyjnych używanych poniżej nie wymaga renderowania strony, więc przebieg triażu nigdy nie musi wygenerować ani jednej bitmapy
procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := IncomingPath;
Pdf.FormFill := False; // bez środowiska formularzy, bez inicjalizacji JavaScriptu
Pdf.Active := True; // cicha porażka: Active po prostu pozostaje False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // uszkodzony plik lub blokada hasłem użytkownika
Exit; // blok finally i tak się wykona
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // nigdy nie wyciekaj instancji przy wadliwym pliku
end;
end;
Sprawdzenie po przypisaniu nie jest opcjonalne i jest sprawdzeniem, a nie obsługą wyjątku — i to nie bez powodu. Gdy silnik nie może wczytać pliku, komponent połyka wewnętrzny EPdfError i zostawia Active na wartości False, zamiast go propagować. Kod czekający na wyjątek z przyjemnością odczyta PageCount z dokumentu, który nigdy się nie otworzył. Jeżeli workflow odrzucania potrzebuje faktycznego komunikatu błędu silnika, wczytaj plik do tablicy bajtów i wywołaj przeciążenie LoadDocument przyjmujące TBytes; ta ścieżka rzeczywiście zgłasza EPdfError z komunikatem, również w przypadku hasła. Try..finally też zasługuje na swoje miejsce. Serwisy przyjmujące dokumenty działają bez nadzoru tygodniami i żaden późniejszy wyjątek nie może spowodować wycieku instancji TPdf ani przytrzymać blokady, o którą potknie się przebieg ponawiania
Przepustowość rzadko staje się wąskim gardłem. Przy wyłączonym wypełnianiu formularzy i bez renderowania otwarcie triażowe zależy głównie od operacji wejścia-wyjścia, a pojedynczy worker spokojnie przeprowadza inspekcję kilku plików na sekundę z lokalnego dysku. Jeśli wolumen przyjmowanych plików kiedyś przerośnie jednego workera, podziel pracę według plików, a nie według sprawdzeń. Pięć pytań współdzieli jedno otwarcie, a ich rozproszenie po procesach przemnożyłoby najdroższy krok zamiast go amortyzować
Metadane żyją w dwóch miejscach i się rozjeżdżają
ISO 32000-1 wyznacza metadanym dokumentu dwa domy: informacyjny słownik dokumentu (punkt 14.3.3) oraz pakiet XMP dołączony do katalogu (punkt 14.3.2). Właściwości Title, Author, Subject i CreationDate czytają słownik Info, z MetaText[] dla dowolnego innego klucza i DecodeDate do parsowania ciągu daty D:YYYYMMDD.... Haczyk tkwi w tym, że nowoczesne programy tworzące zapisują coraz częściej wyłącznie XMP — kierunek, który ISO 32000-2 czyni oficjalnym, oznaczając jako przestarzałe większość kluczy słownika Info w PDF 2.0. W narzędziu przyjmującym objaw jest konkretny: Twój warsztat pokazuje pusty tytuł, podczas gdy Adobe Acrobat wyświetla tytuł, bo Acrobat sięgnął po dc:title wewnątrz pakietu XMP, którego właściwości słownika Info nigdy nie dotykają
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // wartość ze słownika Info
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // surowy ciąg daty PDF ("D:2026...")
// Pusty tytuł w Info nie oznacza, że dokument nie ma tytułu.
// Komponent nie udostępnia pakietu XMP, więc zbadaj surowe bajty
// pliku pod kątem elementu dc:title, zanim zaufasz pustce.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Nawet tak prymitywna sonda podciągowa jak powyższa broni się swoją użytecznością: „metadane są, ale nie tam, gdzie szukają starsze narzędzia” to fakt istotny dla kierowania w każdym potoku archiwizacyjnym indeksującym po tytule lub autorze. Jeśli Twój dalszy indeks czyta wyłącznie słownik Info, pliki oflagowane w ten sposób po cichu staną się niewyszukiwalne
Zaszyfrowane pliki, które i tak się otwierają
Zaszyfrowany dokument niekoniecznie nie może zostać otwarty. Standardowy moduł bezpieczeństwa (ISO 32000-1, punkt 7.6.3) odróżnia hasło użytkownika, wymagane do otwarcia dokumentu, od hasła właściciela, które jedynie bramkuje uprawnienia, takie jak drukowanie i kopiowanie. Spora część „chronionych” dokumentów biznesowych jest zaszyfrowana hasłem właściciela i pustym hasłem użytkownika. Otwierają się bez monitu, odszyfrowują w całości i polegają na dobrej woli przeglądarek, które dobrowolnie uszanują flagi uprawnień. To polityka, nie ochrona, a stany przyjmowania w Twoim systemie powinny odzwierciedlać tę różnicę
Wykrycie szyfrowania po udanym otwarciu to jedno wywołanie silnika plus zabezpieczenie. FPDF_GetSecurityHandlerRevision(Pdf.Document) zwraca -1 dla plików niechronionych, a w przeciwnym razie numer rewizji modułu bezpieczeństwa; sygnałem potwierdzającym jest Pdf.Permissions zwracające cokolwiek innego niż maska $FFFFFFFF z ustawionymi wszystkimi bitami. Dla plików naprawdę zablokowanych hasłem użytkownika przypisz Password przed ustawieniem Active := True; jeśli otwarcie nadal się nie powiedzie, skieruj plik do stanu zablokowanego, który prosi nadawcę o poświadczenia przez bezpieczny kanał, zamiast ślepo ponawiać próby. I oprzyj się pokusie traktowania „zaszyfrowany” jako automatycznej kwarantanny. W większości branż pracujących na dużej liczbie dokumentów pliki zaszyfrowane, ale otwieralne, są przypadkiem normalnym, a nie podejrzanym
Treść aktywna: JavaScript, XFA i osadzone pliki
Trzy ustalenia zawsze powinny trafiać do decyzji o kierowaniu. Po pierwsze, JavaScript: zdarzenie OnUnsupportedFeature zgłasza funkcje strukturalne, takie jak XFA czy treści 3D, w miarę napotykania ich przez silnik, ale nie wykrywa JavaScriptu; sprawdź zamiast tego JavaScriptActionCount i traktuj wynik różny od zera jako treść aktywną. Po drugie, XFA: gdy FormType zwraca ftXfaFull, widoczne strony to często niewiele więcej niż rendering szablonu XFA, a konwencjonalna ekstrakcja tekstu zobaczy teksty szablonowe zamiast wypełnionych wartości. Po trzecie, załączniki: PDF to format kontenerowy, a AttachmentCount mówi Ci, czy ten konkretny wozi pasażerów
procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
i, PageNo: Integer;
Ext: string;
begin
Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
(FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
Rec.HasForms := Pdf.FormType <> ftNone;
Rec.IsXfa := Pdf.FormType = ftXfaFull;
Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;
// AnnotationCount to właściwość per strona; przejdź strony, aby ją
// zsumować. Załadowanie obiektu strony nic nie renderuje, więc to tanie.
Rec.Annotations := 0;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
Inc(Rec.Annotations, Pdf.AnnotationCount);
end;
Rec.Attachments := Pdf.AttachmentCount;
for i := 0 to Rec.Attachments - 1 do
begin
Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
Include(Rec.Flags, ifDangerousAttachment);
end;
end;
Dwa szczegóły w tej pętli zasługują na uwagę. Nazwa załącznika pochodzi z wnętrza dokumentu, więc nigdy nie używaj jej ponownie jako ścieżki wyjściowej bez wcześniejszego oczyszczenia; osadzona nazwa taka jak ..\..\start.exe to atak path traversal czekający na nieostrożne wywołanie zapisu. A lista blokowania rozszerzeń to czujka, nie gwarancja: jej zadaniem jest wymusić decyzję człowieka, a nie poświadczyć czystość pliku
Przekształcanie sygnałów w stany kierowania
Sprawny model stanów potrzebuje mniej stanów, niż oczekuje większość zespołów: ready (brak blokad, tekst obecny), review (otwarcie się powiodło, ale coś wymaga ludzkiego oka, np. formularz XFA, JavaScript, pusta warstwa tekstu albo tytuł obecny wyłącznie w XMP), blocked (wymagane hasło użytkownika) oraz damaged (otwarcie się nie powiodło). Rejestruj dowody wraz ze stanem. Hash pliku, liczba stron, dokładne flagi i komunikat błędu silnika dla plików uszkodzonych mają znaczenie, bo osoba, która zakwestionuje decyzję kierowania, zrobi to dopiero po tygodniach, wobec pliku, który mógł w międzyczasie zostać podmieniony lub zmodyfikowany
Gdy operator musi obejrzeć plik poddany kwarantannie, nie przekazuj go domyślnej przeglądarce powłoki. Renderuj go w utwardzonym panelu z wyłączonymi skryptami i obsługą linków — to podejście opisane w artykule o budowaniu bezpiecznej powierzchni podglądu PDF w Delphi. A jeśli Twój potok przyjmowania zasila archiwum z wymaganiami zgodności, przebieg triażu to naturalne miejsce na zaplanowanie głębszego sprawdzenia; walidacja preflight wsadowa względem profili PDF/A i PDF/UA podejmuje dokładnie tam, gdzie ta inspekcja się kończy
Strona produktu komponentu obejmuje licencjonowanie, kompletne API inspekcyjne i zestaw demonstracji, w tym inspektor dokumentów w stylu przyjmowania plików: PDFium Component