Един PDF не е просто хартия. Това е контейнер, който може да носи скриптове, които се изпълняват при отваряне на файла, връзки, които стартират външни програми, връзки, които достигат до уеб сървъри, файлове, вложени вътре във файлове, и подпис, който твърди, че документът не е променян, откакто някой е гарантирал за него. Когато даден файл пристигне от източник, който не контролирате, най-безопасният първи ход не е да го рендирате. А да прочетете какво казва файлът за себе си и да изградите инвентар на всичко, което би могъл да се опита да направи, така че човек да може да реши дали изобщо му е мястото във вашия работен процес
Тази статия преминава през статичен, read-only одитен проход върху тази рискова повърхност, използвайки компонента PDFium за Delphi и Lazarus. Одитът никога не рисува страница. Той парсва структурата на документа, изброява частите на файла, които носят поведение, и записва обикновен отчет. Това е разликата между това да помолите непознат да изпразни джобовете си на вратата и това да му се доверите, защото се е усмихнал
Какво е един одит и какво не е
Бъдете наясно с границата. Един sandboxed предварителен преглед (preview) рендира файл при строги ограничения, така че потребителя да може да го погледне, без файлът да докосва останалата част от машината. Един одит идва преди това. Това е инспекция без рендиране, чийто единствен изход е описание на повърхността на заплахата: какви скриптове съществуват, какви действия са свързани към връзките, дали файлът е подписан и колко строго, и какво е прикачено. Изпълнявате го, когато даден документ пресича граница на доверие (trust boundary), при приемане от имейл, формуляр за качване или фийд от партньор, преди който и да е по-късен етап да го отвори наистина
Компонентът зарежда документ по същия начин за одит, както и за всичко останало. Задавате името на файла и го активирате, което парсва данните за кръстосани препратки и каталога на документа, без да рендира нито една страница. Всичко по-долу чете от това заредено, нерендирано състояние
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Incoming_Invoice.pdf';
Pdf.Active := True; // парсва структурата, не рендира нищо
// одитирайте заредения документ тук
finally
Pdf.Free;
end;
end;
JavaScript в документа в дървото на имената (name tree)
Първото нещо, което трябва да се изброи, е кодът. Един PDF може да носи JavaScript на ниво документ: скриптове, които не са прикачени към никоя страница или поле, а към самия документ, съхранявани в дървото /Names под запис /JavaScript. Съобразяващият се (conforming) viewer ги изпълнява при отваряне. Това е механизмът зад дълга поредица от PDF зловреден софтуер, защото позволява на файла да изпълни логика в мига, в който потребителят щракне двукратно върху него, преди да е прочел и дума
Един одитор иска два факта за всеки такъв скрипт: че съществува и какво съдържа. Компонентът излага броя и ви позволява да четете всяко действие като запис, съдържащ името на скрипта и пълното му тяло. Четенето на тялото има значение. Скрипт на име Doc.0 не ви казва нищо, но текстът му може да извика app.launchURL или да сглоби низ и да го подаде някъде, където не би трябвало да отива. Изваждането на изходния код, така че рецензентът да може да го прочете, е целият смисъл на маркирането (flagging) на файл, който изпълнява код при отваряне
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); // пълно тяло, за четене от човек
end;
end;
Файл с нула скриптове на ниво документ не е автоматично безопасен, защото съществуват и скриптове на страници и полета, но файл със скриптове на ниво документ винаги заслужава втори поглед. Самият брой на присъствията (presence count) е полезна порта, а тялото е това, което превръща портата в преценка
Launch и URI действия
Следващото поведение за инвентаризиране живее във връзки и анотации. Два типа действия имат най-голямо значение за одитора. Действие Launch стартира външна програма или отваря локален файл, когато връзката бъде задействана. Действие URI отваря уеб цел. Рецензент, разглеждащ подозрителен документ, трябва да може да види, без да кликва нищо, че даден бутон на страница трета е свързан да стартира cmd.exe или да отвори URL, който не съответства на бранда на страницата
Компонентът класифицира връзките, които намира, и излага типа на действието и целевия път за всяко от тях, така че одитът може да изброи всяко Launch и URI действие с неговата дестинация. Това е отчитане (reporting), а не изпълнение. Одиторът чете действието от структурата и го записва. Той никога не го следва
Контролата viewer, която рендира документи, е мястото, където следването на действие би се случило, и нейната позиция по подразбиране е умишлено предпазлива. Контролата TPdfView има набор LinkOptions, който решава кои типове връзки се задействат автоматично при кликване. По подразбиране е [loAutoGoto, loAutoOpenURI], което означава, че скокове вътре в документа и уеб URL адреси могат да се отварят, но loAutoLaunch отсъства, така че Launch действия никога не се изпълняват автоматично. За работен процес на одит отивате по-далеч и изчиствате набора изцяло, така че нищо изобщо не се задейства автоматично, докато все още решавате дали да се доверите на файла
// Одитна позиция за viewer-а: нищо не се изпълнява автоматично, нищо не се отваря автоматично.
View.LinkOptions := [];
// Доставената стойност по подразбиране вече задържа (withholds) launch:
// default = [loAutoGoto, loAutoOpenURI]
// loAutoLaunch НЕ е в набора по подразбиране, така че външни програми
// никога не се стартират при случаен клик по подразбиране (out of the box).
Мотивите зад задържането на launch по подразбиране са прости. Скок вътре в документа е безвреден, а URL е видим и може да бъде отменен, но стартирането на произволна външна програма от кликване е най-опасното нещо, което PDF връзка може да поиска, така че то е изключено, освен ако не се съгласите изрично (opt in). Един одитор се отказва дори от безопасните поведения, защото работата му е да гледа, а не да действа
Нивото на разрешение MDP на цифровия подпис
Подписите променят въпроса. Обикновен подпис удостоверява байтовете към момента на подписване. Сертифициращ подпис, видът, създаден с правило за откриване и предотвратяване на модификации на документа (MDP), отива по-далеч: той декларира какво може легитимно да се промени, след като документът е бил сертифициран, и съобразяващ се viewer предупреждава, ако нещо извън това разрешение е било докоснато. Четенето на това ниво на разрешение казва на одитора дали файлът е сертифициран и, ако е така, колко заключен (locked down) е предвидено да бъде
Разрешението MDP е цяло число с три дефинирани стойности. Ниво 1 означава, че не са разрешени никакви промени; всяка модификация нарушава сертификацията. Ниво 2 разрешава попълване на формуляри и подписване, често срещан случай за договор, който е предназначен да бъде попълнен и подписан, но не променян по друг начин. Ниво 3 допълнително разрешава анотации върху попълването на формуляри и подписването. Познаването на нивото позволява на вашата логика за приемане (intake logic) да разсъждава относно намерението: документ, сертифициран на ниво 1, който въпреки това носи полета на формуляри или скриптове, си противоречи, и това противоречие си струва да бъде маркирано
Компонентът чете броя на подписите и излага всеки като запис, чието поле Permission носи тази MDP стойност, попълнена директно от базовото (underlying) извикване FPDFSignatureObj_GetDocMDPPermission. Разрешение от нула означава, че подписът не е сертифициращ (DocMDP) подпис, така че няма заключване на ниво документ за отчитане
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;
Одитът тук не валидира криптографията на подписа; проверката на сертификатната верига е отделна грижа. Това, което той отчита, е декларираното намерение: този файл казва, че е заключен на това ниво. Точно това е контекстът, от който един рецензент се нуждае, за да прецени дали по-късни промени или самото присъствие на активно съдържание са съвместими с това как авторът е запечатал документа
Останалата част от повърхността: вградени файлове и XFA
Още два елемента допълват пълния инвентар. Вградените файлове са цели документи, носени вътре в PDF-а като прикачени файлове, и те са класическо средство за доставка, защото един безобидно изглеждащ отчет може да достави изпълним файл или втори злонамерен PDF в своето дърво от прикачени файлове. Компонентът излага броя на прикачените файлове и името на всеки от тях, така че одитът може да изброи какво се вози заедно с него (riding along), без да извлича или отваря каквото и да било от това
Присъствието на XFA е другият флаг. Една XFA форма заменя статичния AcroForm с базирана на XML архитектура на формуляри, която носи свой собствен модел за рендиране и скриптиране, по-голяма и по-сложна повърхност от обикновен формуляр. Не е необходимо да обработвате XFA, за да отбележите, че е там; самото му присъствие е сигнал, че файлът носи по-богат интерактивен слой, заслужаващ по-отблизо поглед. Компонентът го отчита като единствен 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;
Една read-only рутина, която записва отчет
Сглобете парчетата и одитът е една-единствена процедура, която зарежда документ, изброява неговите скриптове и техните тела, изброява неговите Launch и URI цели, отчита нивото на MDP на подписа, отбелязва прикачени файлове и XFA, и записва констатациите в журнал (log). Не рендира нищо, така че е евтино и не може да бъде подмамено да покаже враждебно съдържание на страницата. Изходът е плосък, четим от човека запис, въз основа на който рецензент или правило надолу по веригата (downstream) може да действа
Формата, която работи добре на практика, е да събирате всяка констатация като ред, да поставяте префикс на истински рисковите, така че да се сортират най-отгоре в опашката за преглед, и да запазвате (persist) цялото нещо до файла. Документ без скриптове, без Launch действия, без прикачени файлове, без XFA и или без подпис, или с последователна сертификация преминава тихо. Документ, който задейства (trips) няколко флага наведнъж, е този, който човек трябва да види, преди някой по-късен етап да го отвори. Одитът не взема решението за доверие вместо вас. Той гарантира, че решението е информирано, а не сляпо
След като даден файл премине одита и наистина трябва да го погледнете, направете го при ограничения, а не в viewer по подразбиране. Подходът в нашето ръководство за изграждане на сигурен PDF предварителен преглед (preview) в Delphi показва как да попречите на автоматичното управление на връзки и активното съдържание да действат по време на контролиран поглед. За да сгънете (fold) това изброяване в пълен конвейер за приемане (intake pipeline) с инструменти за рецензент, вижте статията за работна маса (workbench) за приемане и преглед на PDF. И двете надграждат върху една и съща read-only, render-free основа и се доставят като част от компонента PDFium за Delphi и C++Builder, заедно с API-тата за рендиране, текст, формуляри и подписи, обхванати другаде в този блог