Articol tehnic

Previzualizare securizată PDF în Delphi cu componenta PDFium

Previzualizarea unui PDF de încredere necunoscută în interiorul propriei dvs. aplicații este o decizie de execuție, iar partea care contează nu este interfața vizualizatorului, ci ceea ce panoul refuză să facă de la sine. Nu scrieți fișierul pe disc. Nu lăsați linkurile lui să lanseze programe externe. Nu îi dați atașamentelor sale o cale. Cea mai mare parte a daunelor produse de un document ostil nu provine dintr-un exploit al motorului, ci dintr-un vizualizator care face lucruri perfect obișnuite cu intrare furnizată de atacator: deschiderea unui link file:// către o partajare UNC care scurge credențiale NTLM, lăsarea unei copii intermediare în directorul temporar, copierea payload-urilor încorporate oriunde îi indică un șir de nume de fișier. PDFium Component este un vizualizator PDF cu cod sursă pentru Delphi, C++Builder și Lazarus, iar el pune comutatoarele relevante la îndemâna dvs.: un indicator la momentul încărcării care omoară scriptarea, evenimente de clic pe link pe care le puteți respinge, acces la atașamente care trece prin propriul dvs. cod și biți de permisiune pe care îi puteți citi. Ordinea de mai jos urmărește un document din momentul în care ajunge până în momentul în care un utilizator dă clic pe ceva din el

Modelul de amenințare al unui panou de previzualizare

Fiți sinceri în privința a ceea ce vă oferă cu adevărat o „previzualizare securizată”. Randatorul analizează octeți de încredere necunoscută indiferent ce faceți, iar întărirea proprie a motorului este podeaua pe care stați. Tot ce se află deasupra acelei podele este politică de aplicație: dacă scripturile se inițializează, ce face un clic pe link, dacă fișierele încorporate pot ajunge pe disc, dacă clipboard-ul și imprimanta sunt uși sau ziduri. Un lucru de scos din calcul de la bun început este comutatorul motorului FPDF_SetSandBoxPolicy. Majoritatea restricțiilor motorului sunt compilate în el, comutatorul schimbă puțin în practică, iar bugetarea vreunei părți din povestea dvs. de izolare pe seama lui produce doar un fals sentiment că ați făcut ceva. Când intrarea este cu adevărat ostilă, de exemplu un portal public de încărcare, singura izolare reală este randarea într-un proces separat cu privilegii reduse și trimiterea bitmap-urilor către interfață. Indicatorii din interiorul procesului sunt politică. Nu sunt izolare

Diagramă PDFium Component contrastând comutatoarele de politică de previzualizare PDF în proces cu redarea în afara procesului într-un muncitor cu privilegii scăzute pentru documentele ostile
Flag-urile in-process ridică bara pentru expeditorii cunoscuți, în timp ce upload-urile anonime justifică un worker separat cu privilegii reduse care expediază doar bitmap-uri către UI

Există două suprafețe ușor de uitat, exact pentru că niciun clic nu le atinge vreodată. Prima sunt fișierele temporare. Dacă pipeline-ul dvs. pune documentele primite pe disc înainte de previzualizare, acele copii intermediare supraviețuiesc sesiunii, dacă nimic nu le șterge verificabil, iar un fișier care este „recuperabil din directorul temporar” a înfrânt în tăcere fiecare control pe care panoul îl aplică el însuși. Încărcați în schimb din memorie prin TPdfStreamAdapter, astfel încât octeții ostili să nu primească niciodată o cale proprie. A doua este clipboard-ul. O previzualizare care permite selectare și copiere a exportat deja documentul, un ecran plin odată, iar nicio interceptare de link nu prinde asta

Opriți JavaScript la momentul încărcării, nu în interfață

JavaScript-ul documentului din PDFium Component se inițializează doar împreună cu mediul form-fill. Încărcarea cu FormFill := False dezactivează deci scriptarea la rădăcină, în loc să îi suprime simptomele:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // fără mediu de formular, deci fără motor JavaScript
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // cuvânt brut de indicatori; toți biții setați = fără restricții
end;

Compromisul este real și aparține specificației dvs. Cu form fill dezactivat, dispar și interacțiunea AcroForm legitimă și scripturile de validare; câmpurile se randează cu ultimul lor aspect salvat, dar nu pot fi editate. Pentru un panou de previzualizare, aceasta este de obicei decizia corectă, deoarece previzualizare înseamnă privire, nu completare. Dar dacă aceeași fereastră servește dublu ca suprafață de completare a formularelor pentru documente interne de încredere, răspunsul este două căi de încărcare cu o decizie explicită de încredere între ele, nu o singură cale cu o setare de compromis prea permisivă pentru cazul ostil și prea strictă pentru cel de încredere. Partea de completare a formularelor din această separare are propriile capcane, tratate în navigarea câmpurilor de formular și regenerarea aspectului

Linkuri: handler-ul implicit lansează programe externe

Lăsate netratate, clicurile pe linkuri merg direct la sistemul de operare. LinkOptions implicite ale vizualizatorului includ loAutoOpenURI, ceea ce este exact scurgerea de la file:// către o partajare UNC așteptând să se întâmple. Două evenimente formează punctul de strangulare: OnWebLinkClick pentru URL-urile detectate în textul paginii, și OnAnnotationLinkClick pentru adnotările de tip link care poartă acțiuni URI sau de lansare. Setați Handled := True în ambele, necondiționat, înainte de a decide orice, apoi re-permiteți doar ce permite politica. Ca al doilea strat, eliminați loAutoOpenURI din LinkOptions pentru intrare ostilă și asigurați-vă că loAutoLaunch, dezactivat implicit, nu se strecoară niciodată înapoi printr-o configurație copiată:

Diagramă de flux a interceptării click-urilor de link PDF într-un panou de previzualizare Delphi, cu o verificare de prefix de schemă pe șir brut și jurnalizare de audit a link-urilor blocate
Stabilirea Handled în ambele evenimente de link ține fiecare click sub politica aplicației, iar o verificare de prefix pe șirul brut ține schemele file:// și UNC în afara
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // nu lăsați niciodată să treacă prin comportamentul implicit al shell-ului

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

Două detalii decid dacă acest lucru chiar rezistă. Primul, verificarea schemei trebuie să fie o verificare de prefix pe șirul brut, înainte de orice analiză, deoarece file://, căile UNC și schemele exotice sunt exact valorile care blochează un parser URL naiv sau se strecoară printr-unul care normalizează prea agresiv. Al doilea, jurnalizați fiecare blocare cu identitatea documentului atașată. O mână de linkuri file:// blocate este zgomot de fundal; o rafală a lor pe parcursul multor documente primite într-o fereastră scurtă de timp este un incident despre care echipa dvs. de securitate ar prefera să audă de la dvs., nu din altă parte

Atașamente: politica de extensii și numele de fișier pe care nu l-ați ales dvs

Un PDF este un container, iar AttachmentCount împreună cu proprietatea AttachmentName[] vă spune ce poartă înainte ca ceva să atingă discul. Aici contează două controale separate, și doar unul dintre ele este evident. Cel evident este politica de tip: o listă albă de extensii care pot fi vreodată exportate. Cel subtil este că numele atașamentului este date controlate de atacator, punct. Un nume încorporat precum ..\..\Startup\update.exe transformă o salvare neatentă într-un path traversal care depune un executabil într-un folder pe care Windows îl rulează la logare. Componenta vă oferă payload-ul ca octeți prin Attachment[] și lasă codul dvs. să aleagă calea, așa că construiți acea cale dintr-un nume de bază igienizat, niciodată din șirul brut încorporat:

Diagramă a conductei PDFium Component, igienizând un nume de atașament PDF controlat de atacator prin ExtractFileName și o listă albă de extensii înainte de a scrie octeții
Numele atașamentului încorporat este input de atacator, astfel încât calea de export este reconstruită dintr-un basename curățat și gătuită de o listă albă de extensii fail-closed
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // elimină orice componente de cale
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // listă albă, nu listă neagră
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // payload încorporat ca octeți bruți
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Preferați direcția listei albe. O listă neagră de extensii „periculoase” este o cursă pe care o pierdeți în ziua în care cineva transformă în armă o extensie despre care nu ați auzit niciodată; o listă albă de .pdf, .png și .csv eșuează în siguranță (fail closed)

Ce promit de fapt permisiunile de criptare

Handler-ul de securitate standard din ISO 32000-1 codifică indicatori de permisiune pentru tipărire, copierea conținutului și modificare, iar proprietățile Permissions și UserPermissions le expun ca măști de biți brute odată ce documentul se deschide. Tabelul 22 din ISO 32000-1 definește biții, iar un fișier necriptat raportează fiecare bit setat. Citiți-i și respectați-i în stratul dvs. de comenzi, dar fiți clari cu privire la ce sunt ei. Pentru un document criptat cu o parolă de proprietar și o parolă de utilizator goală, conținutul se decriptează complet la deschidere, iar indicatorii sunt o cerere adresată vizualizatoarelor conforme, nu un mecanism de impunere. Asta are două consecințe, care trag în direcții opuse. Nu prezentați niciodată utilizatorilor indicatorii de permisiune ca pe o proprietate de securitate a documentelor pe care le primesc, deoarece nu sunt așa ceva. În același timp, respectați bitul de extragere pentru accesibilitate (bitul 10) chiar și acolo unde copierea generală (bitul 5) este refuzată; accesul pentru cititoarele de ecran este exceptat separat, intenționat, în modelul de permisiuni, iar eliminarea lui pentru că „copierea este dezactivată” strică tehnologia asistivă fără niciun câștig de securitate

Impuneți acțiunile refuzate la nivelul comenzii, nu ascunzând butoane din bara de instrumente. Ctrl+C, meniurile contextuale și selecția prin tragere ocolesc toate o bară de instrumente; o singură verificare de permisiune în interiorul comenzii de copiere nu ocolește nimic

Pentru documentele care chiar necesită o parolă de utilizator, atribuiți Password înainte de Active := True și tratați valoarea ca secretul care este: preluați-o din magazinul dvs. de credențiale per sesiune, țineți-o departe de jurnale și rapoarte de crash și nu o persistați niciodată lângă document. Un panou de previzualizare care stochează parole în cache „pentru comoditate” a devenit în tăcere o bază de date de parole fără niciuna dintre protecțiile uneia

Tipărirea merită propria decizie, în loc să moștenească orice a rezultat din regula de copiere. Un exemplar tipărit fizic este neauditat prin definiție, totuși blocarea totală a tipăririi tinde să împingă utilizatorii spre capturi de ecran, care sunt mai rele din toate punctele de vedere. Un compromis obișnuit este să permiteți tipărirea, dar să ștampilați fiecare pagină cu identitatea utilizatorului și un timestamp, impuse în interiorul comenzii de tipărire. Doar păstrați așteptarea corectă pentru asta: un filigran este descurajare și atribuire. Nu este prevenire

Ce ar fi trebuit să vă spună deja recepția

Un panou de previzualizare ia decizii mai bune atunci când fișierul sosește cu un dosar deja atașat: criptat sau nu, JavaScript prezent sau absent, un recensământ al atașamentelor, tipul formularului. Acea trecere de inspecție aparține în amonte de vizualizator, iar tiparul din construirea unui banc de lucru pentru revizuirea la recepție a PDF-urilor produce exact indicatorii pe care o politică de previzualizare vrea să îi consume. Fișierele pe care recepția le-a marcat drept riscante se deschid automat prin calea întărită; documentele de rutină își păstrează avantajele. Legați cele două etape de un singur obiect de politică partajat, în loc de două ecrane de configurare, care se vor îndepărta unul de celălalt până la a doua versiune, oricât de atent le-ați scrie prima dată

Unde cade linia dintre in-process și out-of-process depinde de cine vă trimite fișierele. Pentru recepția obișnuită de afaceri, persoanele care trimit documente sunt cunoscute și doar neatente, iar previzualizarea in-process cu scriptarea dezactivată și linkurile interceptate este o bară apărabilă. Pentru încărcările publice anonime, nu este, și nicio cantitate de setare de indicatori in-process nu o face una; randați-le într-un worker separat cu privilegii reduse și trimiteți bitmap-uri către interfață, astfel încât un defect al motorului să vă coste un worker, nu aplicația gazdă. Decideți acea separare în mod deliberat și consemnați în ce categorie cade fiecare cale de ingestie, deoarece costul unei presupuneri greșite este asimetric

Licențierea, suprafața de API legată de securitate și o demonstrație de vizualizator întărit se află pe pagina de produs: PDFium Component