Technical Article

PDF-veiligheidsrisico's auditen met PDFium in Delphi

Een PDF is niet zomaar papier. Het is een container die scripts kan bevatten die worden uitgevoerd wanneer het bestand wordt geopend, links die externe programma's starten, links die verbinding maken met webservers, bestanden die in bestanden zijn genest, en een handtekening die verklaart dat het document niet is gewijzigd sinds iemand er garant voor stond. Wanneer een bestand binnenkomt van een bron die u niet beheert, is de veiligste eerste stap niet om het te renderen. Het is om te lezen wat het bestand over zichzelf zegt en een inventarisatie te maken van alles wat het zou kunnen proberen te doen, zodat een mens kan beslissen of het überhaupt in uw workflow thuishoort.

Dit artikel bespreekt een statische, alleen-lezen audit van dat risico-oppervlak met behulp van het PDFium-component voor Delphi en Lazarus. De audit tekent nooit een pagina. Het parseert de documentstructuur, inventariseert de delen van het bestand die gedrag vertonen, en schrijft een eenvoudig rapport. Het is het verschil tussen een vreemde vragen zijn zakken leeg te maken bij de deur of hem vertrouwen omdat hij lachte.

Wat een audit is, en wat het niet is

Wees duidelijk over de grens. Een sandbox-preview rendert een bestand onder strikte beperkingen, zodat een gebruiker ernaar kan kijken zonder dat het bestand de rest van de machine beïnvloedt. Een audit komt daarvóór. Het is een inspectie zonder rendering waarvan de enige uitvoer een beschrijving van het bedreigingsoppervlak is: welke scripts er bestaan, welke acties aan links zijn gekoppeld, of het bestand is ondertekend en hoe streng, en wat er is bijgevoegd. U voert dit uit wanneer een document een betrouwbaarheidsgrens overschrijdt, bij ontvangst via e-mail, een uploadformulier of een partner-feed, voordat een latere fase het daadwerkelijk opent.

Het component laadt een document voor een audit op dezelfde manier als voor al het andere. U stelt de bestandsnaam in en activeert deze, wat de kruisverwijzingsgegevens (cross-reference) en de documentcatalogus parseert zonder een enkele pagina te renderen. Alles hieronder leest vanuit die geladen, niet-gerenderde status.

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;

Document-JavaScript in de naamboom

Het eerste dat moet worden geïnventariseerd is code. Een PDF can JavaScript op documentniveau bevatten: scripts die niet aan een pagina of veld zijn gekoppeld, maar aan het document zelf, opgeslagen in de /Names-boom onder een /JavaScript-vermelding. Een conformerende viewer voert deze uit bij het openen. Dat is het mechanisme achter een lange reeks PDF-malware, omdat het een bestand in staat stelt logica uit te voeren op het moment dat een gebruiker erop dubbelklikt, nog voordat deze een woord heeft gelezen.

Een auditor wil twee feiten weten over elk van dergelijke scripts: dat het bestaat, en wat het bevat. Het component toont het aantal en stelt u in staat elke actie te lezen als een record dat de naam van het script en de volledige inhoud (body) ervan bevat. Het lezen van de inhoud is belangrijk. Een script met de naam Doc.0 zegt u niets, maar de tekst ervan kan app.launchURL aanroepen of een string samenstellen en ergens naartoe sturen waar dat niet zou moeten. Het eruit halen van de broncode zodat een beoordelaar deze kan lezen, is de hele reden om een bestand te markeren dat code uitvoert bij het openen.

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;

Een bestand met nul document-scripts is niet automatisch veilig, want er bestaan ook pagina- en veldscripts, maar een bestand met document-scripts verdient altijd een tweede blik. Het aantal aanwezige scripts alleen is een nuttige drempel, en de inhoud is wat die drempel omzet in een oordeel.

Launch- en URI-acties

Het volgende gedrag dat moet worden geïnventariseerd bevindt zich op links en annotaties. Twee actietypen zijn het belangrijkst voor een auditor. Een Launch-actie start een extern programma of opent een lokaal bestand wanneer de link wordt geactiveerd. Een URI-actie opent een webdoel. Een beoordelaar die naar een verdacht document kijkt, moet zonder ergens op te klikken kunnen zien dat een knop op pagina drie is gekoppeld om cmd.exe te starten of om een URL te openen die niet overeenkomt met het merk op de pagina.

Het component classificeert de links die het vindt en toont het actietype en het doelpad voor elk, zodat een audit elke Launch- en URI-actie met de bestemming ervan kan vermelden. Dit is rapportage, geen uitvoering. De auditor leest de actie uit de structuur en noteert deze. Hij volgt deze nooit.

Het viewer-besturingselement dat documenten rendert is de plek waar het volgen van een actie zou plaatsvinden, en de standaardhouding is bewust voorzichtig. Het besturingselement TPdfView heeft een LinkOptions-set die bepaalt welke linktypen automatisch worden geactiveerd bij een klik. De standaardwaarde is [loAutoGoto, loAutoOpenURI], wat betekent dat sprongen binnen het document en web-URL's kunnen worden geopend, maar loAutoLaunch ontbreekt, waardoor startacties (launch actions) nooit automatisch worden uitgevoerd. Voor een audit-workflow gaat u verder en maakt u de set volledig leeg, zodat er helemaal niets automatisch wordt geactiveerd terwijl u nog beslist of u het bestand vertrouwt.

// 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.

De reden om starten standaard te weigeren is eenvoudig. Een sprong binnen het document is onschadelijk en een URL is zichtbaar en kan worden geannuleerd, maar het starten van een willekeurig extern programma via een klik is het gevaarlijkste wat een PDF-link kan vragen. Het staat dus uit, tenzij u er expliciet voor kiest. Een auditor kiest ervoor om zelfs de veilige gedragingen uit te schakelen, omdat het werk eruit bestaat te kijken, en niet te handelen.

Het MDP-machtigingniveau van de digitale handtekening

Handtekeningen veranderen de vraag. Een gewone handtekening getuigt van de bytes op het moment van ondertekening. Een certificeringshandtekening, het type dat is gemaakt met een regel voor het detecteren en voorkomen van documentwijzigingen (document modification detection and prevention), gaat verder: deze verklaart wat er legitiem mag veranderen nadat het document is gecertificeerd, en een conforme viewer waarschuwt als er iets buiten die toestemming is aangeraakt. Het lezen van dat machtigingsniveau vertelt een auditor of een bestand is gecertificeerd en, zo ja, hoe streng het is beveiligd.

De MDP-machtiging is een integer met drie gedefinieerde waarden. Een niveau van 1 betekent dat er helemaal geen wijzigingen zijn toegestaan; elke wijziging verbreekt de certificering. Een niveau van 2 staat het invullen en ondertekenen van formulieren toe, wat gebruikelijk is voor een contract dat moet worden ingevuld en ondertekend, maar verder niet mag worden gewijzigd. Een niveau van 3 staat daarnaast annotaties toe bovenop het invullen en ondertekenen van formulieren. Het kennen van het niveau stelt uw ontvangstlogica in staat om om de intentie na te denken: een document dat op niveau 1 is gecertificeerd maar desondanks formuliervelden of scripts bevat, spreekt zichzelf tegen, en die tegenstrijdigheid is het markeren waard.

Het component leest het aantal handtekeningen en toont elk als een record waarvan het Permission-veld die MDP-waarde bevat, rechtstreeks ingevuld vanuit de onderliggende FPDFSignatureObj_GetDocMDPPermission-aanroep. Een machtiging van nul betekent dat de handtekening geen certificeringshandtekening (DocMDP) is, dus er is geen documentvergrendeling om te rapporteren.

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;

Een audit verifieert hier niet de cryptografie van de handtekening; het verifiëren van de certificaathandtekeningketen is een aparte zorg. Wat het rapporteert is de verklaarde intentie: dit bestand zegt dat het op dit niveau is vergrendeld. Dat is precies de context die een beoordelaar nodig heeft om te beoordelen of latere wijzigingen, of de loutere aanwezigheid van actieve inhoud, consistent zijn met hoe de auteur het document heeft verzegeld.

De rest van het risico-oppervlak: ingebedde bestanden en XFA

Nog twee items maken een volledige inventarisatie compleet. Ingebedde bestanden zijn hele documenten die als bijlagen binnen de PDF worden meegestuurd. Ze zijn een klassiek verspreidingsmiddel, omdat een onschuldig uitziend rapport een uitvoerbaar bestand of een second schadelijke PDF in de bijlagenstructuur kan bevatten. Het component toont het aantal bijlagen en de naam van elke bijlage, zodat de audit kan vermelden wat er meereist zonder dat er iets van wordt uitgepakt of geopend.

De aanwezigheid van XFA is de andere vlag. Een XFA-formulier vervangt de statische AcroForm door een op XML gebaseerde formulierarchitectuur die een eigen rendering- en scriptingmodel met zich meebrengt, een groter en complexer oppervlak dan een gewoon formulier. U hoeft de XFA niet te verwerken om te registreren dat deze er is; de loutere aanwezigheid ervan is een signaal dat het bestand een rijkere interactieve laag bevat die een nadere blik waard is. Het component rapporteert dit als een enkele boolean.

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;

Eén alleen-lezen routine die een rapport schrijft

Breng de onderdelen samen en de audit is een enkele procedure die een document laadt, de bijbehorende scripts en hun inhoud (bodies) inventariseert, de Launch- en URI-doelen vermeldt, het MDP-niveau van de handtekening rapporteert, bijlagen en XFA noteert, en de bevindingen naar een logboek schrijft. Het rendert niets, dus het is goedkoop en kan niet worden misleid om kwaadaardige pagina-inhoud weer te geven. De uitvoer is een plat, door mensen leesbaar record waarop een beoordelaar of een downstream-regel kan reageren.

De vorm die in de praktijk goed werkt, is om elke bevinding als een regel te verzamelen, de werkelijk risicovolle te voorzien van een prefix zodat ze naar de bovenkant van een beoordelingswachtrij worden gesorteerd, en het geheel naast het bestand op te slaan. Een document zonder scripts, zonder startacties, zonder bijlagen, zonder XFA, en met ofwel geen handtekening ofwel een gecodeerde certificering passeert geruisloos. Een document dat meerdere vlaggen tegelijk activeert, is degene die een persoon zou moeten zien voordat een latere fase het opent. De audit neemt de beslissing over vertrouwen niet voor u. Het zorgt ervoor dat de beslissing geïnformeerd is in plaats van blind.

Zodat een bestand de audit heeft doorstaan en u het wel moet bekijken, doe dit dan onder restricties in plaats van in een standaardviewer. De aanpak in onze handleiding over het bouwen van een beveiligde PDF-preview in Delphi laat zien hoe u automatische linkverwerking en actieve inhoud kunt uitschakelen tijdens een gecontroleerde weergave. Om deze inventarisatie op te nemen in een volledig ontvangsttraject met beoordelingshulpmiddelen, zie het artikel over het PDF-ontvangst- en beoordelingsplatform. Beide bouwen voort op dezelfde alleen-lezen, rendering-vrije basis en worden geleverd als onderdeel van de PDFium Component voor Delphi en C++Builder, naast de API's voor rendering, tekst, formulieren en handtekeningen die elders op deze blog worden behandeld.