Műszaki cikk

Biztonságos PDF-előnézet Delphi alkalmazásokban

Egy nem megbízható PDF előnézete a saját alkalmazásunkon belül végrehajtási döntés, és nem a megjelenítő kerete számít, hanem az, amit a panel magától megtagad. Ne írjuk lemezre a fájlt. Ne engedjük, hogy a hivatkozásai a rendszerhéjhoz forduljanak. Ne adjunk útvonalat a mellékleteinek. Egy ellenséges dokumentum kárának nagy része nem motorsérülékenységből fakad, hanem abból, hogy egy megjelenítő teljesen hétköznapi dolgokat művel támadó által megadott bemenettel: megnyit egy file:// hivatkozást egy UNC-megosztásra, amely NTLM hitelesítő adatokat szivárogtat, ideiglenes könyvtárban hagy egy előkészített másolatot, és oda másolja a beágyazott rakományt, ahová egy fájlnév-karakterlánc mondja. A PDFium Component forráskódos PDF-megjelenítő Delphihez, C++Builderhez és Lazarushoz, és elérhető helyre teszi a fontos kapcsolókat: egy betöltéskori jelzőt, amely megöli a szkriptelést, hivatkozáskattintási eseményeket, amelyeket megvétózhatunk, mellékletelérést, amely a saját kódunkon fut keresztül, és jogosultsági biteket, amelyeket kiolvashatunk. Az alábbi sorrend egy dokumentumot követ attól a pillanattól, hogy megérkezik, addig, amíg a felhasználó rákattint benne valamire

Egy előnézeti panel fenyegetésmodellje

Legyünk őszinték abban, mit ad a „biztonságos előnézet”. A megjelenítő akárhogy is nem megbízható bájtokat elemez, a motor saját megerősítése pedig az a padló, amelyen állunk. Minden, ami e padló fölött van, alkalmazásszintű szabályzat: inicializálódnak-e a szkriptek, mit tesz egy hivatkozáskattintás, elérhetik-e a beágyazott fájlok a lemezt, ajtó vagy fal-e a vágólap és a nyomtató. Egy dolgot érdemes korán leírni: a motor FPDF_SetSandBoxPolicy kapcsolóját. A motor korlátozásainak többsége fordítási időben rögzül, a kapcsoló a gyakorlatban keveset változtat, és ha az elszigetelési történetünk bármely részét rá építjük, azzal csak azt az érzést szerezzük meg, hogy tettünk valamit. Amikor a bemenet valóban ellenséges – mondjuk egy nyilvános feltöltőportál –, az egyetlen valódi elszigetelés az, ha külön, alacsony jogosultságú folyamatban jelenítjük meg, és bitképeket küldünk a felületnek. A folyamaton belüli jelzők szabályzatot jelentenek. Nem elzárást

PDFium Component ábra, amely a folyamaton belüli PDF-előnézeti szabályzatkapcsolókat állítja szembe az ellenséges dokumentumok külön, alacsony jogosultságú munkafolyamatban történő megjelenítésével
A folyamaton belüli jelzők ismert küldőknél emelik a lécet, míg a névtelen feltöltések indokolttá teszik a külön, alacsony jogosultságú munkafolyamatot, amely csak bitképeket küld a felületnek

Két felületről pontosan azért könnyű megfeledkezni, mert egyetlen kattintás sem érinti őket. Az első az ideiglenes fájlok. Ha a folyamatunk előnézet előtt lemezre készíti elő a beérkező dokumentumokat, ezek az előkészített másolatok túlélik a munkamenetet, hacsak valami igazolhatóan nem törli őket, és az a fájl, amely „az ideiglenes könyvtárból visszaszerezhető”, csendben legyőzte a panel minden vezérlését. Töltsünk be inkább a memóriából a TPdfStreamAdapter segítségével, hogy az ellenséges bájtok soha ne kapjanak saját útvonalat. A második a vágólap. Az az előnézet, amely engedi a kijelölést és a másolást, már exportálta a dokumentumot, képernyőnyi darabokban, és ezt semmilyen hivatkozáselfogás nem kapja el

Öljük meg a JavaScriptet betöltéskor, ne a felületen

A PDFium Component eszközben a dokumentum JavaScriptje csak az űrlapkitöltési környezettel együtt inicializálódik. A FormFill := False beállítással történő betöltés ezért gyökerénél kapcsolja ki a szkriptelést, nem a tüneteit fojtja el:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // nincs űrlapkörnyezet, tehát nincs JavaScript-motor
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // nyers jelzőszó; minden bit beállítva = korlátlan
end;

A kompromisszum valós, és a specifikációnkban a helye. Kikapcsolt űrlapkitöltés mellett a jogos AcroForm-interakció és az ellenőrző szkriptek is eltűnnek; a mezők az utoljára mentett megjelenésükkel rajzolódnak ki, de nem szerkeszthetők. Egy előnézeti panelnél ez rendszerint a helyes döntés, hiszen az előnézet nézést jelent, nem kitöltést. De ha ugyanaz az ablak megbízható belső dokumentumok űrlapkitöltő felületeként is szolgál, a válasz két betöltési út, közöttük kifejezett bizalmi döntéssel – nem pedig egyetlen út olyan kompromisszumos beállítással, amely az ellenséges esetre túl laza, a megbízhatóra pedig túl szoros. Az elágazás űrlapkitöltő oldalának saját csapdái vannak, ezeket az űrlapmező-bejárás és megjelenés-újragenerálás tárgyalja

Hivatkozások: az alapértelmezett kezelő a rendszerhéjhoz fordul

Magára hagyva a hivatkozáskattintás egyenesen az operációs rendszerhez megy. A megjelenítő alapértelmezett LinkOptions beállítása tartalmazza a loAutoOpenURI értéket, ami maga a file:// és UNC-megosztás felé irányuló, bekövetkezésre váró szivárgás. Két esemény alkotja a szűk keresztmetszetet: az OnWebLinkClick az oldalszövegben felismert URL-ekre, és az OnAnnotationLinkClick az URI- vagy indítóműveletet hordozó hivatkozásjegyzetekre. Állítsuk mindkettőben Handled := True értékre, feltétel nélkül, mielőtt bármit eldöntenénk, majd csak azt engedjük vissza, amit a szabályzat megenged. Második rétegként ellenséges bemenetnél vegyük ki a loAutoOpenURI értéket a LinkOptions beállításból, és ügyeljünk rá, hogy az alapból kikapcsolt loAutoLaunch soha ne kússzon vissza egy átmásolt konfiguráción keresztül:

A PDF-hivatkozáskattintások elfogásának folyamatábrája egy Delphi előnézeti panelen, nyers karakterláncon végzett séma-előtagellenőrzéssel és a letiltott hivatkozások naplózásával
Ha mindkét hivatkozáseseményben beállítjuk a Handled értéket, minden kattintás az alkalmazás szabályzata alatt marad, a nyers karakterláncon végzett előtagellenőrzés pedig kirekeszti a file:// és UNC sémákat
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // soha ne essünk át az alapértelmezett héjviselkedésre

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

Két részleten múlik, hogy ez valóban tart-e. Először: a sémaellenőrzésnek a nyers karakterláncon végzett előtagellenőrzésnek kell lennie, bármiféle elemzés előtt, mert éppen a file://, a UNC-útvonalak és az egzotikus sémák azok az értékek, amelyek megfektetnek egy naiv URL-elemzőt, vagy átcsúsznak azon, amelyik túl buzgón normalizál. Másodszor: naplózzunk minden tiltást a dokumentum azonosságával együtt. Néhány letiltott file:// hivatkozás háttérzaj; egy rövid időn belüli, sok beérkező dokumentumon átívelő özön viszont olyan incidens, amelyről a biztonsági csapatunk szívesebben hall tőlünk, mint máshonnan

Mellékletek: kiterjesztési szabályzat és a fájlnév, amelyet nem mi választottunk

A PDF tároló, az AttachmentCount és az AttachmentName[] tulajdonság pedig még azelőtt megmondja, mit visz, hogy bármi lemezhez érne. Két külön vezérlés számít itt, és csak az egyik kézenfekvő. A kézenfekvő a típusszabályzat: azoknak a kiterjesztéseknek az engedélyezőlistája, amelyek valaha exportálhatók. A finomabb az, hogy a melléklet neve támadó által vezérelt adat, pont. Egy ..\..\Startup\update.exe alakú beágyazott név egy gondatlan mentésből útvonalbejárást csinál, amely futtatható állományt ejt egy olyan mappába, amelyet a Windows bejelentkezéskor futtat. A komponens az Attachment[] tulajdonságon keresztül bájtokként adja át a rakományt, és a kódunkra bízza az útvonalválasztást, ezért az útvonalat tisztított alapnévből építsük, soha ne a nyers beágyazott karakterláncból:

PDFium Component folyamatábra, amely egy támadó által vezérelt PDF-melléklet nevét az ExtractFileName hívással és kiterjesztés-engedélyezőlistával tisztítja meg a bájtok kiírása előtt
A beágyazott mellékletnév támadói bemenet, ezért az exportálási útvonal tisztított alapnévből épül újra, és zárva bukó kiterjesztés-engedélyezőlista kapuzza
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // levágja az útvonalrészeket
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // engedélyezőlista, nem tiltólista
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // a beágyazott rakomány nyers bájtokként
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Válasszuk az engedélyezőlista irányát. A „veszélyes” kiterjesztések tiltólistája olyan verseny, amelyet aznap veszítünk el, amikor valaki fegyverré tesz egy kiterjesztést, amelyről soha nem hallottunk; a .pdf, .png és .csv engedélyezőlistája zárva bukik

Mit ígérnek valójában a titkosítási jogosultságok

Az ISO 32000-1 szabványos biztonsági kezelője jogosultsági jelzőket kódol nyomtatásra, tartalommásolásra és módosításra, a Permissions és UserPermissions tulajdonságok pedig nyers bitmaszkként hozzák felszínre őket, amint a dokumentum megnyílt. Az ISO 32000-1 22. táblázata határozza meg a biteket, egy titkosítatlan fájl pedig minden bitet beállítva jelent. Olvassuk ki és tartsuk tiszteletben őket a parancsrétegünkben, de legyünk tisztában azzal, mik ezek. Egy tulajdonosi jelszóval és üres felhasználói jelszóval titkosított dokumentumnál a tartalom megnyitáskor teljesen visszafejtődik, a jelzők pedig kérések a szabványt követő megjelenítők felé, nem kikényszerítő mechanizmus. Ennek két következménye van, és ellentétes irányba húznak. Soha ne mutassuk be a jogosultsági jelzőket a felhasználóknak a kapott dokumentumok biztonsági tulajdonságaként, mert nem azok. Ugyanakkor tartsuk tiszteletben az akadálymentes kinyerés bitjét (10. bit) ott is, ahol az általános másolás (5. bit) tiltott; a képernyőolvasók hozzáférését a jogosultsági modell szándékosan kezeli külön, és ha „mert a másolás ki van kapcsolva” alapon elvesszük, az minden biztonsági nyereség nélkül töri el a segítő technológiát

A megtagadott műveleteket a parancsok szintjén érvényesítsük, ne az eszköztárgombok elrejtésével. A Ctrl+C, a helyi menük és a húzással kijelölés mind megkerüli az eszköztárat; a másolási parancson belüli egyetlen jogosultsági ellenőrzés semmit nem kerül meg

Azoknál a dokumentumoknál, amelyek valóban felhasználói jelszót kérnek, az Active := True előtt adjuk meg a Password értéket, és kezeljük az értéket annak a titoknak, ami: munkamenetenként kérjük le a hitelesítéstárolónkból, tartsuk távol a naplóktól és az összeomlási jelentésektől, és soha ne tároljuk a dokumentum mellett. Az az előnézeti panel, amely „a kényelem kedvéért” jelszavakat gyorsítótáraz, csendben jelszóadatbázissá vált, egy ilyen adatbázis védelmei nélkül

A nyomtatás önálló döntést érdemel, nem azt, hogy a másolási szabályra ráörökítsük. Egy fizikai nyomat definíció szerint auditálatlan, a nyomtatás teljes tiltása mégis a képernyőképek felé tereli a felhasználókat, ami minden tengelyen rosszabb. Gyakori arany középút az, ha engedjük a nyomtatást, de minden oldalra rábélyegezzük a felhasználó azonosságát és egy időbélyeget, a nyomtatási parancson belül kikényszerítve. Csak legyenek helyesek az elvárásaink: a vízjel elrettentés és eredetazonosítás. Nem megelőzés

Amit az átvételnek már el kellett volna mondania

Egy előnézeti panel jobb döntéseket hoz, ha a fájl már dossziéval együtt érkezik: titkosított-e, van-e benne JavaScript, mellékletek listája, űrlaptípus. Ez a vizsgálati menet a megjelenítő elé tartozik, és a PDF-átvételi vizsgálóműhely építése cikkben leírt minta pontosan azokat a jelzőket állítja elő, amelyeket egy előnézeti szabályzat fogyasztani szeretne. Azok a fájlok, amelyeket az átvétel kockázatosnak jelölt, automatikusan a megerősített úton nyílnak meg; a rutindokumentumok megtartják a kényelmi szolgáltatásaikat. Kössük a két szakaszt egyetlen közös szabályzatobjektumhoz, ne két beállítási képernyőhöz, mert azok a második kiadásra szétcsúsznak, bármilyen gondosan írtuk meg őket először

Az, hogy hol húzódik a határ a folyamaton belüli és a folyamaton kívüli megjelenítés között, attól függ, ki küld nekünk fájlokat. A szokásos üzleti átvételnél a dokumentumok küldői ismertek és legfeljebb gondatlanok, és a folyamaton belüli előnézet kikapcsolt szkripteléssel és elfogott hivatkozásokkal védhető léc. Névtelen nyilvános feltöltéseknél nem az, és semennyi folyamaton belüli jelzőállítás nem teszi azzá; ezeket külön, alacsony jogosultságú munkafolyamatban jelenítsük meg, és bitképeket küldjünk a felületnek, hogy egy motorhiba egy munkafolyamatunkba kerüljön, ne a gazdaalkalmazásba. Ezt az elágazást tudatosan döntsük el, és írjuk le, melyik beviteli út melyik kosárba esik, mert a rossz tipp ára aszimmetrikus

A licencelés, a biztonsággal kapcsolatos API-felület és egy megerősített megjelenítőt bemutató demó a termékoldalon található: PDFium Component