Nástroj na kontrolu prijatých PDF (intake review workbench) je malý program s jedinou úlohou: preveriť každý súbor predtým, než ho spracujú nadväzujúce procesy. Na splnenie tejto úlohy musí v jednom kroku skombinovať niekoľko činností. Otvorí súbor (bez toho, aby mu dôveroval), prečíta informácie, ktoré o sebe súbor tvrdí, hľadá obsah schopný zavádzať jednoduché extraktory alebo prenášať útok, posúdi prítomnosť extrahovateľného textu a následne dokument nasmeruje do príslušného radu na základe svojich zistení. Ak túto kontrolu vynecháte, chyby prebehnú potichu: súbor PDF zašifrovaný heslom vlastníka (owner password), ktorý obsahuje formulár XFA, prejde extraktorom textu ako prázdny reťazec, zaindexuje sa ako prázdny dokument, a nikto si to nevšimne, až kým niekto v ďalšom kroku nezačne hľadať obsah, ktorý nebol nikdy načítaný. PDFium Component je knižnica so zdrojovým kódom na prezeranie a analýzu dokumentov pre prostredia VCL/LCL v Delphi, C++Builder a Lazarus. Sprístupňuje introspektívne volania, ktoré tento nástroj vyžaduje. V nasledujúcich častiach si popíšeme, ktoré volanie odpovedá na ktorú otázku, a ukážeme si dve miesta, kde vám zdanlivo jasná funkcia poskytne sebaisto nesprávny výsledok.
Päť otázok, ktoré treba zodpovedať pred nasmerovaním súboru
Ak odhliadneme od vizuálnych tabuliek a náhľadov stránok, triedenie prijatých dokumentov sa redukuje na päť otázok:
- Dá sa súbor vôbec otvoriť a pod akým heslom?
- Za čo sa dokument vydáva: názov, autor, dátum vytvorenia?
- Obsahuje aktívny alebo rizikový obsah, ako je JavaScript, formulár XFA alebo vnorené súbory?
- Obsahuje extrahovateľný text, alebo ide o sken určený na OCR?
- Na základe toho všetkého, do ktorého radu súbor patrí: na priame spracovanie, manuálnu kontrolu alebo do karantény?
Každá otázka sa mapuje na jedno alebo dve volania komponentu PDFium Component. Dve z týchto mapovaní však majú záludné miesta, ktoré spôsobujú väčšinu nesprávne nasmerovaných súborov, ktoré som musel v produkcii ladiť. Metadáta dokumentu žijú na dvoch rôznych miestach, ktoré sa nemusia zhodovať, a šifrovanie nemusí nevyhnutne brániť otvoreniu dokumentu.
Úsporné otváranie: vypnuté FormFill a nula vykreslených strán
Trieďte s čo najnižšími nákladmi na otvorenie súboru. Nastavenie FormFill := False pred priradením Active := True hovorí komponentu, aby úplne vynechal prostredie na vypĺňanie formulárov. To skracuje čas načítania a (čo je rovnako dôležité pri súboroch neznámeho pôvodu) zabraňuje inicializácii akéhokoľvek JavaScriptu na úrovni dokumentu. Žiadna z analytických vlastností použitých nižšie nevyžaduje vykreslenie stránky, takže triediaci prechod nemusí vytvoriť ani jednu bitovú mapu.
procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := IncomingPath;
Pdf.FormFill := False; // no form environment, no JavaScript init
Pdf.Active := True; // failure is silent: Active simply stays False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // damaged file or user-password lock
Exit; // the finally block still runs
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // never leak the instance on a malformed file
end;
end;
Kontrola po priradení nie je voliteľná. A ide o kontrolu, nie o spracovanie výnimky, z veľmi konkrétneho dôvodu. Keď jadro nedokáže načítať súbor, komponent zachytí internú výnimku EPdfError a ponechá vlastnosť Active na hodnote False namiesto jej poslania ďalej. Kód, ktorý očakáva výnimku, by tak mohol veselo čítať PageCount z dokumentu, ktorý sa v skutočnosti neotvoril. Ak váš schvaľovací proces potrebuje skutočný text chyby jadra, načítajte súbor do poľa bajtov a zavolajte preťaženie LoadDocument s parametrom TBytes. Táto cesta vyvolá výnimku EPdfError s chybovým hlásením, a to aj pri problémoch s heslom. Blok try..finally má stále zmysel. Služby pre príjem súborov bežia bez dozoru celé týždne a žiadna neskoršia výnimka nesmie spôsobiť únik inštancie TPdf alebo zanechať zámok na súbore, na ktorom by zlyhali ďalšie pokusy.
Rýchlosť spracovania (throughput) je málokedy úzkym hrdlom. Pri vypnutom vypĺňaní formulárov a bez vykresľovania dominuje triedeniu I/O komunikácia, takže jeden pracovný proces bez problémov skontroluje niekoľko súborov za sekundu z lokálneho disku. Ak by objem prichádzajúcich súborov predsa len prerástol kapacity jedného procesu, rozdeľte prácu podľa súborov a nie podľa jednotlivých kontrol. Päť položených otázok zdieľa jedno otvorenie súboru a ich rozdelenie do viacerých procesov by najdrahší krok iba vynásobilo namiesto toho, aby ho rozložilo.
Metadáta žijú na dvoch miestach, ktoré sa nemusia zhodovať
Norma ISO 32000-1 definuje dve miesta pre metadáta dokumentu: slovník informácií o dokumente (článok 14.3.3) a balík XMP pripojený k hlavnému katalógu (článok 14.3.2). Vlastnosti Title, Author, Subject a CreationDate čítajú slovník Info, pričom MetaText[] slúži na čítanie akýchkoľvek iných kľúčov a DecodeDate analyzuje dátumový reťazec v tvare D:YYYYMMDD....
Záludnosť spočíva v tom, že moderné editory čoraz častejšie zapisujú iba metadáta typu XMP, čo norma ISO 32000-2 v PDF 2.0 potvrdzuje označením väčšiny kľúčov slovníka Info za zastarané. Príznak v triediacom nástroji je jasný. Váš ovládací panel ukazuje prázdny názov (title), zatiaľ čo Adobe Acrobat ho zobrazuje, pretože Acrobat siahol po záložnej hodnote dc:title vo vnútri balíka XMP, na ktorú vlastnosti čítajúce slovník Info nedosiahnu.
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // Info dictionary value
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // raw PDF date string ("D:2026...")
// An empty Info title does not mean the document is untitled. The
// component does not expose the XMP packet, so probe the raw file
// bytes for the dc:title element before trusting the blank.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Aj takéto jednoduché vyhľadávanie podreťazca má zmysel: informácia „metadáta existujú, ale nie tam, kde ich hľadajú staršie nástroje“ je dôležitým faktom pre smerovanie v archívnych systémoch, ktoré indexujú podľa názvu alebo autora. Ak váš indexovací systém číta iba slovník Info, súbory označené týmto spôsobom sa potichu stanú nevyhľadateľnými.
Zašifrované súbory, ktoré sa napriek tomu dajú otvoriť
Zašifrovaný dokument nemusí nevyhnutne zlyhať pri otváraní. Štandardný bezpečnostný handler (ISO 32000-1 článok 7.6.3) rozlišuje používateľské heslo (user password), potrebné na otvorenie dokumentu, od hesla vlastníka (owner password), ktoré iba obmedzuje práva, ako je tlač a kopírovanie. Veľká časť „chránených“ firemných dokumentov je zašifrovaná heslom vlastníka a prázdnym používateľským heslom. Otvoria sa bez vyzvania, plne sa dešifrujú a spoliehajú sa na to, že prehliadače dobrovoľne dodržia príznaky oprávnení. Ide skôr o pravidlá ako o skutočnú ochranu a váš systém pre príjem súborov by mal tento rozdiel zohľadniť.
Detekcia šifrovania po úspešnom otvorení vyžaduje jedno volanie jadra a jeden záložný mechanizmus. FPDF_GetSecurityHandlerRevision(Pdf.Document) vracia hodnotu -1 pre nechránené súbory a revíziu handlera v ostatných prípadoch, pričom potvrdzujúcim signálom je vlastnosť Pdf.Permissions vracajúca hodnotu inú ako masku so všetkými nastavenými bitmi $FFFFFFFF. Pri súboroch skutočne uzamknutých používateľským heslom priraďte vlastnosť Password pred nastavením Active := True. Ak otvorenie napriek tomu zlyhá, nasmerujte súbor do zablokovaného stavu, ktorý si vyžiada prístupové údaje od odosielateľa cez zabezpečený kanál, namiesto slepého opakovania pokusov. A odolajte pokušeniu považovať „zašifrované“ za automatický dôvod pre karanténu. V odvetviach s vysokým obratom dokumentov sú zašifrované, no voľne otvárateľné súbory bežným stavom, nie podozrivým.
Aktívny obsah: JavaScript, XFA a vnorené súbory
Tri zistenia by mali vždy ovplyvniť rozhodnutie o nasmerovaní súboru. Po prvé, JavaScript: udalosť OnUnsupportedFeature hlási štrukturálne prvky, ako je XFA alebo 3D obsah, hneď ako na ne jadro narazí, no nedokáže detegovať JavaScript. Skontrolujte namiesto toho vlastnosť JavaScriptActionCount a nenulový výsledok považujte za aktívny obsah. Po druhé, XFA: keď vlastnosť FormType vráti ftXfaFull, viditeľné stránky sú často iba vykreslením šablóny XFA a bežná extrakcia textu prečíta iba sprievodný text namiesto vyplnených hodnôt. Po tretie, prílohy: PDF je kontajnerový formát a vlastnosť AttachmentCount vám povie, či so sebou dokument nesie ďalších pasažierov.
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 is a per-page property; walk the pages to total
// it. Loading a page object renders nothing, so this stays cheap.
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;
Dva detaily v tomto cykle si zaslúžia pozornosť. Názov prílohy pochádza z vnútra dokumentu, preto ho nikdy nepoužívajte ako výstupnú cestu bez predchádzajúceho ošetrenia (sanitizácie). Vnútri uložený názov ako ..\..\start.exe je zneužitím relatívnej cesty (path traversal) čakajúcim na neopatrné volanie uloženia. A zoznam zakázaných prípon (blocklist) je len varovným signálom, nie zárukou. Jeho úlohou je vynútiť si rozhodnutie človeka, nie garantovať čistotu súboru.
Premena signálov na stavy smerovania
Použiteľný stavový model vyžaduje menej stavov, než väčšina tímov očakáva: ready (pripravený - bez prekážok, text je prítomný), review (kontrola - otvorenie prebehlo úspešne, ale súbor vyžaduje pozornosť, napr. kvôli formuláru XFA, JavaScriptu, chýbajúcemu textu alebo názvu iba v XMP), blocked (zablokovaný - vyžaduje sa používateľské heslo) a damaged (poškodený - otvorenie zlyhalo). Zaznamenávajte dôkazy spolu so stavom. Haš súboru, počet strán, presné príznaky a chybové hlásenie jadra pre poškodené súbory - to všetko je dôležité, pretože osoba spochybňujúca smerovanie sa ozve až po týždňoch a súbor mohol byť medzitým nahradený alebo upravený.
Keď už operátor musí skontrolovať karanténny súbor, neotvárajte ho v predvolenom prehliadači systému. Vykreslite ho v zabezpečenom paneli s vypnutým skriptovaním a spracovaním odkazov, čo je postup popísaný v článku o vytvorení zabezpečeného náhľadu PDF v Delphi. A ak váš príjem súborov plní archív s požiadavkami na zhodu s normami, triediaci prechod je ideálnym miestom na naplánovanie hlbšej kontroly. Dávková preflight validácia profilov PDF/A a PDF/UA nadväzuje presne tam, kde táto analýza končí.
Produktová stránka komponentu sa venuje licencovaniu, kompletnému inšpekčnému API a sprievodným demám, vrátane inšpektora dokumentov pre príjem: PDFium Component.