Technical Article

Auditarea riscurilor de securitate PDF cu PDFium în Delphi

Un PDF nu înseamnă doar hârtie. Este un container care poate transporta scripturi ce rulează când se deschide fișierul, linkuri care pornesc programe externe, linkuri care accesează servere web, fișiere imbricate în alte fișiere și o semnătură care susține că documentul nu s-a schimbat de când cineva a garantat pentru el. Când un fișier sosește dintr-o sursă pe care nu o controlați, cea mai sigură primă mișcare nu este să îl redați. Este să citiți ce spune fișierul despre el însuși și să construiți un inventar cu tot ceea ce ar putea încerca să facă, astfel încât un om să poată decide dacă își are locul în fluxul dumneavoastră de lucru.

Acest articol parcurge o etapă de audit static, read-only, asupra acelei suprafețe de risc folosind componenta PDFium pentru Delphi și Lazarus. Auditul nu desenează niciodată o pagină. Acesta parsează structura documentului, enumeră părțile din fișier care poartă comportament și scrie un raport simplu. Este diferența dintre a cere unui străin să-și golească buzunarele la ușă și a avea încredere în el pentru că a zâmbit.

Ce este un audit și ce nu este

Fiți clari cu privire la graniță. O previzualizare izolată (sandboxed) redă un fișier sub restricții strânse, astfel încât un utilizator să îl poată privi fără ca fișierul să atingă restul mașinii. Auditul vine înaintea acesteia. Es o inspecție fără redare a cărei singur rezultat este o descriere a suprafeței de amenințare: ce scripturi există, ce acțiuni sunt conectate la linkuri, dacă fișierul este semnat și cât de strâns, și ce este atașat. Îl rulați când un document trece de o graniță de încredere, la preluarea din e-mail, dintr-un formular de încărcare sau dintr-un flux partener, înainte ca orice etapă ulterioară să îl deschidă cu adevărat.

Componenta încarcă un document în același mod pentru un audit ca și pentru orice altceva. Setați numele fișierului și îl activați, ceea ce parsează datele de referință încrucișată și catalogul documentului fără a reda o singură pagină. Tot ce urmează citește din acea stare încărcată, neredată.

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;

JavaScript la nivel de document în arborele de nume

Primul lucru de enumerat este codul. Un PDF poate purta JavaScript la nivel de document: scripturi care nu sunt atașate la nicio pagină sau câmp, ci la documentul în sine, stocate în arborele /Names sub o intrare /JavaScript. Un vizualizator conform rulează aceste scripturi la deschidere. Acesta este mecanismul din spatele unei lungi linii de malware PDF, deoarece permite unui fișier să execute o logică în secunda în care un utilizator face dublu-clic pe el, înainte de a citi vreun cuvânt.

Un auditor dorește două fapte despre fiecare astfel de script: că există și ce conține. Componenta expune numărul și vă permite să citiți fiecare acțiune ca pe o înregistrare care conține numele scriptului și corpul său complet. Citirea corpului contează. Un script numit Doc.0 nu vă spune nimic, dar textul său ar putea apela app.launchURL sau asambla un șir și să îl transmită undeva unde nu ar trebui. Extragerea sursei pentru ca un evaluator să o poată citi este întregul scop al marcării unui fișier care rulează cod la deschidere.

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;

A file with zero document scripts is not automatically safe, because page and field scripts exist too, but a file with document scripts always deserves a second look. The presence count alone is a useful gate, and the body is what turns a gate into a judgement.

Acțiunile Launch și URI

Următorul comportament de inventariat trăiește pe linkuri și adnotări. Două tipuri de acțiuni contează cel mai mult pentru un auditor. O acțiune Launch pornește un program extern sau deschide un fișier local când linkul este declanșat. O acțiune URI deschide o țintă web. Un evaluator care analizează un document suspect ar trebui să poată vedea, fără a face clic pe nimic, că un buton de pe pagina trei este configurat să lanseze cmd.exe sau să deschidă un URL care nu corespunde brandului de pe pagină.

Componenta clasifică linkurile pe care le găsește și expune tipul de acțiune și calea țintă pentru fiecare, astfel încât un audit poate lista fiecare acțiune Launch și URI cu destinația sa. Aceasta este raportare, nu execuție. Auditorul citește acțiunea din structură și o notează. Nu o urmează niciodată.

Controlul de vizualizare care redă documentele este locul unde ar avea loc urmărirea unei acțiuni, iar postura sa implicită este deliberat precaută. Controlul TPdfView are un set LinkOptions care decide ce tipuri de linkuri se declanșează automat la un clic. Valoarea sa implicită este [loAutoGoto, loAutoOpenURI], ceea ce înseamnă că salturile din document și URL-urile web se pot deschide, dar loAutoLaunch este absent, așa că acțiunile de lansare nu rulează niciodată automat. Pentru un flux de lucru de audit mergeți mai departe și goliți complet setul, astfel încât nimic să nu se declanșeze automat în timp ce decideți dacă aveți încredere în fișier.

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

Raționamentul din spatele reținerii lansării în mod implicit este simplu. Un salt în interiorul documentului este inofensiv și un URL este vizibil și poate fi anulat, dar pornirea unui program extern arbitrar printr-un clic este cel mai periculos lucru pe care îl poate solicita un link PDF, așa că este dezactivat dacă nu optați pentru el. Un auditor renunță chiar și la comportamentele sigure, deoarece treaba lui este să privească, nu să acționeze.

Nivelul de permisiune MDP al semnăturii digitale

Semnăturile schimbă întrebarea. O semnătură simplă atestă octeții de la momentul semnării. O semnătură de certificare, cea creată cu o regulă de detectare și prevenire a modificării documentului (document modification detection and prevention), merge mai departe: declară ce se poate schimba în mod legitim după ce documentul a fost certificat, iar un vizualizator conform avertizează dacă s-a atins ceva în afara acelei permisiuni. Citirea acelui nivel de permisiune îi spune unui auditor dacă un fișier este certificat și, dacă da, cât de blocat ar trebui să fie.

Permisiunea MDP este un întreg cu trei valori definite. Un nivel de 1 înseamnă că nu sunt permise modificări deloc; orice modificare anulează certificarea. Un nivel de 2 permite completarea formularelor și semnarea, cazul obișnuit pentru un contract care este destinat să fie completat și semnat, dar nu modificat în alt mod. Un nivel de 3 permite suplimentar adnotări pe lângă completarea formularelor și semnare. Cunoașterea nivelului permite logicii dumneavoastră de preluare să analizeze intenția: un document certificat la nivelul 1 care conține totuși câmpuri de formular sau scripturi se contrazice singur, iar acea contradicție merită semnalată.

Componenta citește numărul de semnături și expune fiecare semnătură ca pe o înregistrare al cărei câmp Permission poartă acea valoare MDP, populată direct din apelul FPDFSignatureObj_GetDocMDPPermission de baz. O permisiune de zero înseamnă că semnătura nu este o semnătură de certificare (DocMDP), deci nu există o blocare la nivel de document de raportat.

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;

Un audit nu validează criptografia semnăturii aici; verificarea lanțului de certificate este o preocupare separată. Ceea ce raportează este intenția declarată: acest fișier spune că a fost blocat la acest nivel. Acesta este exact contextul de care are nevoie un evaluator pentru a judeca dacă modificările ulterioare, sau simpla prezență a conținutului activ, sunt în concordanță cu modul în care autorul a sigilat documentul.

Restul suprafeței: fișiere încorporate și XFA

Încă două elemente completează un inventar complet. Fișierele încorporate sunt documente întregi transportate în interiorul PDF-ului ca atașamente și reprezintă un vehicul de livrare clasic, deoarece un raport cu aspect inofensiv poate transporta un executabil sau un al doilea PDF periculos în arborele său de atașamente. Componenta expune numărul de atașamente și numele fiecărui atașament, astfel încât auditul poate lista ceea ce călătorește alături fără a extrage sau a deschide nimic.

Prezența XFA este celălalt flag. Un formular XFA înlocuiește AcroForm-ul static cu o arhitectură de formulare bazată pe XML care aduce propriul model de redare și scripting, o suprafață mai mare și mai complexă decât un formular simplu. Nu trebuie să procesați XFA pentru a observa că este acolo; simpla sa prezență este un semnal că fișierele poartă un strat interactiv mai bogat care merită o privire mai atentă. Componenta îl raportează ca pe o singură valoare 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;

O singură rutină read-only care scrie un raport

Puneți piesele cap la cap și auditul devine o singură procedură care încarcă un document, enumeră scripturile sale și corpurile acestora, listează țintele Launch și URI, raportează nivelul MDP al semnăturii, notează atașamentele și XFA și scrie constatările într-un jurnal. Nu redă nimic, așa că este ieftin și nu poate fi păcălit să afișeze conținut ostil de pagină. Rezultatul este o înregistrare plată, lizibilă de către om, pe care un evaluator sau o regulă ulterioară o poate procesa.

Forma care funcționează bine în practică este colectarea fiecărei constatări sub formă de linie, prefixarea celor cu adevărat riscante pentru a le sorta în partea de sus a unei cozi de revizuire și păstrarea întregului raport lângă fișier. Un document fără scripturi, fără acțiuni de lansare, fără atașamente, fără XFA și fie fără semnătură, fie cu o certificare coerentă trece fără probleme. Un document care activează mai multe flag-uri deodată este cel pe care o persoană ar trebui să îl vadă înainte ca orice etapă ulterioară să îl deschidă. Auditul nu ia decizia de încredere în locul dumneavoastră. Se asigură însă că decizia este informată, nu oarbă.

Odată ce un fișier trece de audit și chiar trebuie să-l analizați, faceți acest lucru sub restricții în loc de un vizualizator implicit. Abordarea din ghidul nostru despre construirea unei previzualizări PDF sigure în Delphi arată cum să împiedicați gestionarea automată a linkurilor și conținutul activ să acționeze în timpul unei analize controlate. Pentru a integra această enumerare într-un flux complet de preluare cu instrumente de revizuire, consultați articolul despre bancul de lucru pentru preluarea și revizuirea PDF-urilor. Ambele se bazează pe aceeași bază read-only, fără redare, și sunt livrate ca parte a PDFium Component pentru Delphi și C++Builder, alături de API-urile de redare, text, formulare și semnături acoperite în alte părți pe acest blog.