Műszaki cikk

PDF-átvételi vizsgálóműhely építése Delphiben PDFiummal

A PDF-átvételi vizsgálóműhely kis program, egyetlen feladattal: minden fájlra ránéz, mielőtt a folyamat bármely későbbi része hozzáérhetne. Ehhez néhány képességet egyetlen menetbe kell összefognia. Megnyitja a fájlt (anélkül, hogy megbízna benne), elolvassa, mit állít a fájl önmagáról, olyan tartalmat keres, amely félrevezetne egy naiv kinyerőt vagy támadást hordozna, eldönti, van-e egyáltalán kinyerhető szöveg, majd a találtak alapján egy sorba tereli a dokumentumot. Ha kihagyjuk a vizsgálatot, a hibák a csendes fajtából lesznek: egy tulajdonosi jelszóval titkosított PDF, amely XFA-űrlapot burkol, üres karakterláncokként siklik át a szövegkinyerőn, üres dokumentumként indexelődik, és senki nem veszi észre, amíg valaki a folyamat végén olyan tartalmat nem kezd keresni, amelyet soha nem olvastak be. A PDFium Component forráskódos VCL/LCL megjelenítő és vizsgálókönyvtár Delphihez, C++Builderhez és Lazarushoz, és pontosan azokat az önvizsgáló hívásokat teszi elérhetővé, amelyekre ennek a műhelynek szüksége van. Az alábbi szakaszok végigveszik, melyik hívás melyik kérdésre felel, és azt a két helyet, ahol a kézenfekvő hívás magabiztosan rossz választ ad

Öt kérdés, amelyet a fájl terelése előtt meg kell válaszolni

Ha lehántjuk a rácsot és a bélyegképsávot, az átvételi osztályozás öt kérdésre egyszerűsödik:

  • Megnyitható-e egyáltalán a fájl, és melyik jelszóval?
  • Minek vallja magát: cím, szerző, létrehozási dátum?
  • Hordoz-e aktív vagy kockázatos tartalmat, például JavaScriptet, XFA-űrlapot vagy beágyazott fájlokat?
  • Van-e kinyerhető szöveg, vagy szkennelt anyag, amely OCR felé tart?
  • Mindezek alapján melyik sorba kerül: átmenő feldolgozás, kézi ellenőrzés vagy karantén?
Egy Delphi PDF-átvételi műhely ábrája, amely öt osztályozási kérdést válaszol meg egyetlen olcsó megnyitásból, és a fájlokat kész, ellenőrzendő, tiltott vagy sérült állapotba tereli
Az átvételi osztályozás egyetlen olcsó megnyitásból válaszol meg öt kérdést, és a fájlt kész, ellenőrzendő, tiltott vagy sérült állapotba tereli

Minden kérdés egy vagy két PDFium Component hívásra képződik le. Ezek közül két leképezésnek éles sarkai vannak, és ezek adják azoknak a rosszul terelt fájloknak a többségét, amelyeket éles üzemben hibakeresnem kellett. A dokumentum metaadatai két különböző helyen élnek, amelyek ellentmondhatnak egymásnak, a titkosítás pedig nem feltétlenül akadályozza meg a dokumentum megnyitását

Nyissunk olcsón: űrlapkitöltés kikapcsolva, nulla kirajzolt oldal

Az osztályozásnak a lehető legolcsóbb megnyitásnak kell lennie. Ha az Active := True előtt beállítjuk a FormFill := False értéket, azzal közöljük a komponenssel, hogy teljesen hagyja ki az űrlapkitöltési környezetet. Ez rövidíti a betöltési időt, és (ismeretlen eredetű fájloknál legalább ennyire fontos) megakadályozza, hogy bármilyen dokumentumszintű JavaScript inicializálódjon. Az alább használt vizsgálati tulajdonságok egyike sem igényli oldal kirajzolását, így egy osztályozási menetnek soha nem kell egyetlen bitképet sem előállítania

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // nincs űrlapkörnyezet, nincs JavaScript-indítás
    Pdf.Active := True;        // a hiba néma: az Active egyszerűen False marad

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // sérült fájl vagy felhasználói jelszavas zár
      Exit;                    // a finally blokk ettől még lefut
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // hibás fájlnál se szivárogjon el a példány
  end;
end;

Az értékadás utáni ellenőrzés nem elhagyható, és nem véletlenül ellenőrzés, nem kivételkezelő. Amikor a motor nem tudja betölteni a fájlt, a komponens elnyeli a belső EPdfError kivételt, és False értéken hagyja az Active tulajdonságot ahelyett, hogy továbbadná. Az a kód, amely kivételre vár, vidáman kiolvassa a PageCount értéket egy soha meg nem nyílt dokumentumból. Ha az elutasítási munkafolyamathoz a motor tényleges hibaszövege kell, olvassuk a fájlt bájttömbbe, és hívjuk azt a LoadDocument túlterhelést, amely TBytes paramétert vesz át; az az út már kivált EPdfError kivételt az üzenettel együtt, a jelszavas esetet is beleértve. A try..finally ettől még kiérdemli a helyét. Az átvételi szolgáltatások hetekig futnak felügyelet nélkül, és semmilyen későbbi kivétel nem szivárogtathatja el a TPdf példányt, és nem tarthat olyan zárolást, amelybe az újrapróbálkozó menet beleütközik

Az áteresztőképesség ritkán válik szűk keresztmetszetté. Kikapcsolt űrlapkitöltéssel és kirajzolás nélkül az osztályozó megnyitást az I/O uralja, és egyetlen munkaszál kényelmesen több fájlt vizsgál meg másodpercenként helyi lemezről. Ha az átvételi mennyiség mégis kinőné az egy munkaszálat, fájlonként osszuk fel a munkát, ne ellenőrzésenként. Az öt kérdés egyetlen megnyitáson osztozik, és a folyamatok közti szétdarabolásuk a legdrágább lépést sokszorozná meg ahelyett, hogy szétterítené

A metaadatok két helyen élnek, és ellentmondanak egymásnak

Az ISO 32000-1 két otthont határoz meg a dokumentum metaadatainak: a dokumentuminformációs szótárt (14.3.3 pont) és a katalógushoz csatolt XMP-csomagot (14.3.2 pont). A Title, Author, Subject és CreationDate tulajdonságok az Info szótárt olvassák, minden más kulcshoz a MetaText[] tartozik, a D:YYYYMMDD... alakú dátum-karakterlánc elemzéséhez pedig a DecodeDate. A bökkenő az, hogy a korszerű előállítók egyre inkább csak XMP-t írnak, és ezt az irányt az ISO 32000-2 hivatalossá is teszi azzal, hogy a PDF 2.0 szabványban az Info szótár legtöbb kulcsát elavulttá nyilvánítja. Egy átvételi eszközben a tünet kézzelfogható. A műhelyünk üres címet mutat, miközben az Adobe Acrobat kiír egyet, mert az Acrobat visszaesett az XMP-csomagon belüli dc:title elemre, amelyhez az Info szótárt olvasó tulajdonságok soha nem nyúlnak

Ábra arról, hogy a PDF metaadatai két helyen élnek, az Info szótárban és az XMP-csomagban, amelyek egy Delphi átvételi eszközben ellentmondhatnak egymásnak a címről
A dokumentum metaadatai az Info szótárban és az XMP-csomagban élnek, és a két otthon ellentmondhat egymásnak a címet illetően
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // az Info szótár értéke
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // nyers PDF dátum-karakterlánc ("D:2026...")

  // Az üres Info-cím nem jelenti azt, hogy a dokumentum címtelen. A
  // komponens nem teszi elérhetővé az XMP-csomagot, ezért a nyers fájl
  // bájtjaiban keressük a dc:title elemet, mielőtt hinnénk az üresnek.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Még a fenti nyers részkarakterláncos próba is megszolgálja a helyét: a „van metaadat, csak nem ott, ahol a régi eszközök keresik” tény terelési szempontból jelentős minden olyan archiváló folyamatnál, amely cím vagy szerző szerint indexel. Ha a lejjebb álló index csak az Info szótárt olvassa, az így megjelölt fájlok némán kereshetetlenné válnak

Titkosított fájlok, amelyek mégis megnyílnak

Egy titkosított dokumentum nem feltétlenül bukik el a megnyitáson. A szabványos biztonsági kezelő (ISO 32000-1, 7.6.3 pont) megkülönbözteti a felhasználói jelszót, amely a dokumentum megnyitásához kell, a tulajdonosi jelszótól, amely csupán olyan jogosultságokat zár el, mint a nyomtatás és a másolás. A „védett” üzleti dokumentumok jelentős része tulajdonosi jelszóval és üres felhasználói jelszóval van titkosítva. Ezek kérdezés nélkül megnyílnak, teljesen visszafejtődnek, és arra hagyatkoznak, hogy a megjelenítők önként tiszteletben tartják a jogosultsági jelzőket. Ez szabályzat, nem védelem, és az átvételi állapotainknak tükrözniük kell a különbséget

A titkosítás felismerése sikeres megnyitás után egy motorhívásba és egy tartalék jelbe kerül. Az FPDF_GetSecurityHandlerRevision(Pdf.Document) hívás védtelen fájlokra -1 értéket ad, egyébként a kezelő revízióját, megerősítő jel pedig az, ha a Pdf.Permissions nem a mindenbites $FFFFFFFF maszkot adja vissza. Valóban felhasználói jelszóval zárt fájloknál az Active := True beállítása előtt adjuk meg a Password értéket; ha a megnyitás továbbra is elbukik, tereljük a fájlt tiltott állapotba, amely biztonságos csatornán kér hitelesítő adatot a küldőtől, ahelyett hogy vakon próbálkoznánk újra. És álljunk ellen a kísértésnek, hogy a „titkosított” jelzőt automatikus karanténnak vegyük. A dokumentumnehéz iparágak többségében a titkosított, de megnyitható fájl a normális eset, nem a gyanús

Aktív tartalom: JavaScript, XFA és beágyazott fájlok

Három megállapításnak mindig el kell jutnia a terelési döntésig. Először a JavaScript: az OnUnsupportedFeature esemény olyan szerkezeti jellemzőkről számol be, mint az XFA vagy a 3D tartalom, ahogy a motor rájuk fut, de a JavaScriptet nem ismeri fel. Helyette a JavaScriptActionCount értéket nézzük, és a nem nulla eredményt tekintsük aktív tartalomnak. Másodszor az XFA: amikor a FormType az ftXfaFull értéket adja, a látható oldalak gyakran alig többek az XFA-sablon kirajzolásánál, és a hagyományos szövegkinyerés a kitöltött értékek helyett sablonszöveget lát. Harmadszor a mellékletek: a PDF tárolóformátum, és az AttachmentCount mondja meg, hogy ez éppen utasokat visz-e

A PDF-átvétel kockázati jeleinek ábrája Delphiben: a titkosítási kezelő revíziója, a JavaScript-műveletek száma, az XFA űrlaptípus és a veszélyes mellékletek
A titkosítási állapot, valamint a JavaScript-, XFA- és mellékletszámok azok a jelek, amelyeknek el kell jutniuk a terelési döntésig
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;

  // Az AnnotationCount oldalankénti tulajdonság; járjuk be az oldalakat az
  // összegzéshez. Egy oldalobjektum betöltése semmit nem rajzol, így ez olcsó marad.
  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;

Ebben a ciklusban két részlet érdemel figyelmet. A melléklet neve a dokumentum belsejéből jön, ezért soha ne használjuk fel kimeneti útvonalként előzetes tisztítás nélkül; egy ..\..\start.exe alakú beágyazott név útvonalbejárás, amely egy gondatlan mentéshívásra vár. A kiterjesztések tiltólistája pedig botlódrót, nem garancia. A feladata az, hogy emberi döntést kényszerítsen ki, nem az, hogy tisztának minősítse a fájlt

Jelekből terelési állapotok

Egy működőképes állapotmodellhez kevesebb állapot kell, mint a legtöbb csapat várná: kész (nincs akadály, van szöveg), ellenőrzendő (a megnyitás sikerült, de valamire szem kell, például XFA-űrlapra, JavaScriptre, üres szövegrétegre vagy csak XMP-ben szereplő címre), tiltott (felhasználói jelszó kell) és sérült (a megnyitás elbukott). Az állapot mellé rögzítsük a bizonyítékot is. A fájl hasítóértéke, az oldalszám, a pontos jelzők és sérült fájloknál a motor hibaüzenete mind számít, mert aki megkérdőjelez egy terelési döntést, azt hetekkel később teszi, egy olyan fájllal szemben, amelyet azóta lecserélhettek vagy módosíthattak

Amikor egy kezelőnek mégis rá kell néznie egy karanténba tett fájlra, ne az alapértelmezett rendszermegjelenítőnek adjuk oda. Jelenítsük meg megerősített felületen, kikapcsolt szkriptkezeléssel és hivatkozáskezeléssel – ezt a megközelítést írja le a biztonságos PDF-előnézeti felület építése Delphiben. Ha pedig az átvételünk megfelelőségi követelményekkel bíró archívumot táplál, az osztályozó menet a természetes hely a mélyebb ellenőrzés ütemezésére; a kötegelt előellenőrzés PDF/A és PDF/UA profilok szerint pontosan ott folytatja, ahol ez a vizsgálat abbahagyja

A komponens termékoldala tárgyalja a licencelést, a teljes vizsgálati API-t és a mellékelt demókat, köztük egy átvételi jellegű dokumentumvizsgálót: PDFium Component