En PDF är inte bara papper. Det är en behållare som kan bära skript som körs när filen öppnas, länkar som startar externa program, länkar som når ut till webbservrar, filer nästlade inuti filer och en signatur som intygar att dokumentet inte har ändrats sedan någon gick i god för det. När en fil anländer från en källa du inte kontrollerar, är det säkraste första steget inte att rendera den. Det är att läsa vad filen säger om sig själv och bygga en inventering av allt den kan försöka göra, så att en människa kan avgöra om den överhuvudtaget hör hemma i ditt arbetsflöde.
Denna artikel går igenom en statisk, skrivskyddad granskningsomgång (audit pass) över den riskytan med hjälp av PDFium-komponenten för Delphi och Lazarus. Granskningen ritar aldrig en sida. Den tolkar dokumentstrukturen, räknar upp de delar av filen som bär på beteenden och skriver en enkel rapport. Det är skillnaden mellan att be en främling tömma sina fickor vid dörren och att lita på dem för att de log.
Vad en granskning är, och vad det inte är
Var tydlig med gränsen. En sandlådeförhandsgranskning (sandboxed preview) renderar en fil under strikta begränsningar så att en användare kan titta på den utan att filen rör vid resten av maskinen. En granskning (audit) kommer före det. Det är en renderingsfri inspektion vars enda utdata är en beskrivning av hotytan: vilka skript som finns, vilka åtgärder som är kopplade till länkar, om filen är signerad och hur hårt, samt vad som är bifogat. Du kör den när ett dokument korsar en förtroendegräns, vid mottagning från e-post, ett uppladdningsformulär eller ett partnerflöde, innan något senare skede öppnar det på riktigt.
Komponenten läser in ett dokument på samma sätt för en granskning som för allt annat. Du anger filnamnet och aktiverar den, vilket tolkar korsreferensdatan (cross-reference data) och dokumentkatalogen utan att rendera en enda sida. Allt nedan läser från det inlästa, orenderade tillståndet.
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;
Dokument-JavaScript i namnträdet
Det första som ska räknas upp är kod. En PDF kan bära JavaScript på dokumentnivå: skript som inte är kopplade till någon sida eller fält utan till själva dokumentet, lagrade i /Names-trädet under en /JavaScript-post. Ett kompatibelt visningsprogram kör dessa vid öppning. Det är mekanismen bakom en lång rad PDF-skadlig kod, eftersom det låter en fil köra logik i samma ögonblick som en användare dubbelklickar på den, innan de ens har läst ett ord.
En granskare vill ha två fakta om varje sådant skript: att det finns, och vad det innehåller. Komponenten exponerar antalet och låter dig läsa varje åtgärd som en post (record) som innehåller skriptets namn och dess fullständiga kropp. Att läsa kroppen är viktigt. Ett skript med namnet Doc.0 säger ingenting, men dess text kan anropa app.launchURL eller sätta ihop en sträng och skicka den dit den inte borde gå. Att dra ut källkoden så att en granskare kan läsa den är hela poängen med att flagga en fil som kör kod vid öppning.
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;
En fil med noll dokumentskript är inte automatiskt säker, eftersom sid- och fältskript också förekommer, men en fil med dokumentskript förtjänar alltid en extra titt. Antalet närvarande skript är i sig en användbar grind, och kroppen är det som förvandlar en grind till en bedömning.
Launch- och URI-åtgärder
Nästa beteende att inventera lever på länkar och annoteringar. Två åtgärdstyper är viktigast för en granskare. En Launch-åtgärd startar ett externt program eller öppnar en lokal fil när länken utlöses. En URI-åtgärd öppnar ett webbmål. En granskare som tittar på ett misstänkt dokument bör kunna se, utan att klicka på något, att en knapp på sida tre är kopplad för att starta cmd.exe eller för ått öppna en URL som inte matchar varumärket på sidan.
Komponenten klassificerar länkarna den hittar och exponerar åtgärdstyp och målsökväg för var och en, så att en granskning kan lista varje Launch- och URI-åtgärd med dess destination. Detta är rapportering, inte körning. Granskaren läser åtgärden ur strukturen och skriver ner den. Den följer den aldrig.
Visningskontrollen (viewer control) som renderar dokument är den plats där följande av en åtgärd skulle ske, och dess standardinställning är medvetet försiktig. Kontrollen TPdfView har en LinkOptions-mängd som avgör vilka länktyper som utlöses automatiskt vid ett klick. Dess standardvärde är [loAutoGoto, loAutoOpenURI], vilket innebär att hopp inom dokumentet och webb-URL:er kan öppnas, men loAutoLaunch saknas, så startåtgärder (launch actions) körs aldrig automatiskt. För ett granskningsarbetsflöde går du längre och rensar mängden helt, så att ingenting alls utlöses automatiskt medan du fortfarande beslutar om du ska lita på filen.
// 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.
Resonemanget bakom att hålla inne launch-åtgärder som standard är enkelt. Ett hopp inom dokumentet är ofarligt och en URL is synlig och kan avbrytas, men att starta ett godtyckligt externt program från ett klick är det absolut farligaste en PDF-länk kan be om, så det är avstängt om du inte väljer att aktivera det. En granskare väljer bort även de säkra beteendena, eftersom jobbet är att titta, inte att agera.
Den digitala signaturens MDP-behörighetsnivå
Signaturer förändrar frågan. En vanlig signatur intygar byten vid signeringstiden. En certifieringssignatur (certification signature), den typ som skapas med en regel för identifiering och förebyggande av dokumentändringar (DocMDP), går längre: den deklarerar vad som legitimt får ändras efter att dokumentet har certifierats, och en kompatibel läsare varnar om något utanför det tillåtna har rörts. Att läsa den behörighetsnivån talar om för en granskare om en fil är certifierad och, i så fall, hur låst den är tänkt att vara.
MDP-behörigheten är ett heltal med tre definierade värden. En nivå på 1 betyder att inga ändringar är tillåtna alls; varje ändring bryter certifieringen. En nivå på 2 tillåter ifyllnad av formulär och signering, vilket är det vanliga fallet för ett kontrakt som är tänkt att fyllas i och signeras men inte ändras på annat sätt. En nivå på 3 tillåter dessutom annoteringar utöver ifyllnad av formulär och signering. Att känna till nivån låter din intagslogik resonera om avsikt: ett dokument certifierat på nivå 1 som ändå bär på formulärfält eller skript motsäger sig självt, och den motsägelsen är värd att flagga.
Komponenten läser antalet signaturer och exponerar var och en som en post (record) vars Permission-fält bär det där MDP-värdet, befolkat direkt från det underliggande FPDFSignatureObj_GetDocMDPPermission-anropet. En behörighet på noll betyder att signaturen inte är en certifieringssignatur (DocMDP), så det finns ingen dokumentlåsning att rapportera.
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;
Granskningen validerar inte kryptografin för signaturen här; att verifiera certifikatkedjan är en separat fråga. Det den rapporterar är den deklarerade avsikten: denna fil säger att den var låst på denna nivå. Det är exakt den kontext en granskare behöver för att bedöma om senare ändringar, eller bara närvaron av aktivt innehåll, stämmer överens med hur författaren förseglade dokumentet.
Resten av ytan: inbäddade filer och XFA
Ytterligare två saker kompletterar en fullständig inventering. Inbäddade filer är hela dokument som ligger inuti PDF-filen som bilagor, och de är en klassisk leveransmetod, eftersom en till synes harmlös rapport kan bära med sig en körbar fil eller en annan skadlig PDF i sitt bilagsträd. Komponenten exponerar antalet bilagor och namnet på varje bilaga, så att granskningen kan lista vad som följer med utan att extrahera eller öppna något av det.
Närvaron av XFA är den andra flaggan. Ett XFA-formulär ersätter det statiska AcroForm med en XML-baserad formulärstruktur som för med sig en större och mer komplex renderings- och skriptmodell än ett vanligt formulär. Du behöver inte bearbeta XFA för att notera dess närvaro; dess blotta existens är en signal om att filen bär på ett rikare interaktivt lager som är värt en närmare titt. Komponenten rapporterar det som en boolesk variabel.
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;
En skrivskyddad rutin som skriver en rapport
Lägg samman delarna och granskningen är en enskild procedur som läser in ett dokument, räknar upp dess skript och deras kroppar, listar dess Launch- och URI-mål, rapporterar signaturens MDP-nivå, noterar bilagor och XFA samt skriver resultaten till en logg. Den renderar ingenting, så den är billig och kan inte luras till att visa fientligt sidinnehåll. Utdatan är en platt, mänskligt läsbar post som en granskare eller en regel i ett senare skede kan agera på.
Formen som fungerar bäst i praktiken är att samla varje fynd på en rad, lägga till ett prefix på de som är genuint riskabla så att de sorteras till toppen av en granskningskö, och spara allt bredvid filen. Ett dokument utan skript, utan startåtgärder, utan bilagor, utan XFA och med antingen ingen signatur eller en sammanhängande certifiering passerar tyst. Ett dokument som löser ut flera flaggor samtidigt är det som en person bör titta på innan något senare skede öppnar det. Granskningen fattar inte förtroendebeslutet åt dig. Den ser till att beslutet är fattat med information snarare än i blindo.
När en fil har klarat granskningen och du faktiskt behöver titta på den, bör du göra det under restriktioner snarare än i en standardläsare. Tillvägagångssättet i vår genomgång av hur man bygger en säker PDF-förhandsgranskning i Delphi visar hur man förhindrar att automatisk länkhantering och aktivt innehåll agerar under en kontrollerad visning. För att vika in denna inventering i ett fullständigt intagsflöde med granskningsverktyg, se artikeln om arbetsbänk för PDF-intag och granskning. Båda bygger på samma skrivskyddade, renderingsfria grund och levereras som en del av PDFium Component för Delphi och C++Builder, tillsammans med API:erna för rendering, text, formulär och signaturer som beskrivs på andra ställen i den här bloggen.