PDF файлът не е просто хартия. Той е контейнер, който може да носи скриптове, изпълняващи се при отваряне на файла, връзки, стартиращи външни програми, връзки, свързващи се с уеб сървъри, файлове, вложени в други файлове, и подпис, твърдящ, че документът не се е променил, откакто някой е гарантирал за него. Когато даден файл пристигне от източник, който не контролирате, най-безопасният първи ход не е неговото рендериране. Той е да прочетете какво казва файлът за себе си и да изградите инвентар на всичко, което би могъл да се опита да направи, така че човек да реши дали той изобщо има място във вашия работен процес.
Тази статия преминава през статичен одит само за четене на тази повърхност на риска, използвайки компонента PDFium за Delphi и Lazarus. Одитът никога не рисува страница. Той парсира структурата на документа, изброява частите от файла, които носят поведение, и записва прост отчет. Разликата е същата като да помолите непознат да изпразни джобовете си на вратата и да му се доверите, защото се е усмихнал.
Какво е одитът и какво не е
Бъдете ясни относно границата. Предварителен преглед в пясъчник (sandboxed preview) рендерира файл при строги ограничения, така че потребителят да може да го разгледа, без файлът да докосва останалата част на машината. Одитът идва преди това. Той е инспекция без рендериране, чийто единствен изход е описание на повърхността на заплахата: кои скриптове съществуват, кои действия са свързани с връзки, дали файлът е подписан и колко строго, и какво е прикачено към него. Стартирате го, когато даден документ премине границата на доверие, при приемане от имейл, форма за качване или партньорски фийд, преди всеки следващ етап да го отвори реално.
Компонентът зарежда документ за одит по същия начин, както за всичко останало. Задавате името на файла и го активирате, което парсира данните за кръстосани препратки и каталога на документа, без да рендерира нито една страница. Всичко по-долу чете от това заредено, нерендерирано състояние.
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 в дървото на имената
Първото нещо, което трябва да се изброи, е кодът. PDF файлът може да носи JavaScript на ниво документ: скриптове, които не са прикачени към конкретна страница или поле, а към самия документ, съхранявани в дървото /Names под записа /JavaScript. Съвместимият четец ги изпълнява при отваряне. Това е механизмът зад дълга поредица от зловреден софтуер в PDF, тъй като позволява на файла да изпълни логика в момента, в който потребителят щракне двукратно върху него, преди дори да е прочел и дума.
Одиторът се нуждае от два факта за всеки такъв скрипт: че той съществува и какво съдържа. Компонентът разкрива броя и ви позволява да прочетете всяко действие като запис, съдържащ името на скрипта и пълното му тяло. Четенето на тялото е важно. Скрипт с име Doc.0 не ви казва нищо, но неговият текст може да извиква app.launchURL или да сглобява низ и да го предава там, където не трябва да отива. Извличането на изходния код, така че рецензентът да може да го прочете, е цялата цел на маркирането на файл, който изпълнява код при отваряне.
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;
Флаг с нула скриптове на ниво документ не е автоматично безопасен, тъй като съществуват и скриптове за страници и полета, но файл със скриптове на ниво документ винаги заслужава втори поглед. Само по себе си количеството им е полезна проверка, а тялото на скрипта е това, което превръща проверката в решение.
Действия за стартиране (Launch) и URI действия
Следващото поведение, което трябва да бъде инвентаризирано, живее във връзките и анотациите. Две действия са най-важни за одитора. Действието за стартиране (Launch action) стартира външна програма или отваря локален файл при задействане на връзката. Действието URI отваря уеб адрес. Рецензент, който разглежда подозрителен документ, трябва да може да види, без да щраква върху нищо, че бутон на страница три е свързан да стартира cmd.exe или да отвори URL адрес, който не съвпада с марката на страницата.
Компонентът класифицира връзките, които намира, и разкрива типа на действието и целевия път за всяка една, така че одитът да може да изброи всяко действие Launch и URI с неговата дестинация. Това е докладване, а не изпълнение. Одиторът чете действието от структурата и го записва. Той никога не го следва.
Контролата на четеца, която рендерира документите, е мястото, където би се случило следването на дадено действие, а нейното поведение по подразбиране е умишлено предпазливо. Контролата TPdfView има свойство LinkOptions, което определя кои видове връзки се задействат автоматично при щракване. Неговата стойност по подразбиране е [loAutoGoto, loAutoOpenURI], което означава, че вътрешните преходи в документа и уеб адресите могат да се отварят, но loAutoLaunch липсва, така че действията за стартиране никога не се изпълняват автоматично. За работен процес на одит можете да отидете по-далеч и да изчистите напълно този списък, така че нищо да не се задейства автоматично, докато все още решавате дали да се доверите на файла.
// 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.
Обосновката зад спирането на стартирането по подразбиране е проста. Преходът вътре в документа е безвреден, а уеб адресът е видим и може да бъде отменен, но стартирането на произволна външна програма от щракване е едно от най-опасните неща, които една PDF връзка може да поиска, така че то е изключено, освен ако не го разрешите изрично. Одиторът се отказва дори от безопасните поведения, защото задачата му е да гледа, а не да действа.
Ниво на MDP права на цифровия подпис
Подписите променят нещата. Обикновеният подпис удостоверява байтовете към момента на подписване. Сертифициращият подпис (certification signature), създаден с правило за откриване и предотвратяване на промени в документа (modification detection and prevention - MDP), отива по-далеч: той декларира какво може законно да се промени след сертифицирането на документа, а съвместимият четец предупреждава, ако е променено нещо извън това разрешение. Прочитането на това ниво на права казва на одитора дали файлът е сертифициран и, ако е така, колко строго е предназначен да бъде заключен.
Разрешението MDP е цяло число с три дефинирани стойности. Ниво 1 означава, че изобщо не са разрешени промени; всяко модифициране нарушава сертифицирането. Ниво 2 позволява попълване на форми и подписване - общият случай за договор, който трябва да бъде попълнен и подписан, но не и променян по друг начин. Ниво 3 допълнително позволява добавяне на анотации освен попълването на форми и подписването. Знанието за нивото позволява на вашата логика да разсъждава за намерението: документ, сертифициран на ниво 1, който въпреки това носи полета на форми или скриптове, си противоречи сам и това противоречие си струва да бъде маркирано.
Компонентът чете броя на подписите и разкрива всеки от тях като запис, чието поле Permission носи тази MDP стойност, попълнена директно от базовото извикване 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 в своето дърво от прикачени файлове. Компонентът разкрива броя на прикачените файлове и името на всеки един, така че одитът да може да изброи какво пътува заедно с документа, без да извлича или отваря нищо от него.
Присъствието на XFA е другият флаг. XFA формата заменя статичната AcroForm с XML-базирана архитектура за форми, която носи собствено рендериране и модел за скриптове - по-голяма и по-сложна повърхност от обикновената форма. Не е необходимо да обработвате XFA, за да отбележите, че тя е там; самото ѝ присъствие е сигнал, че файлът носи по-богат интерактивен слой, който си струва да се разгледа по-отблизо. Компонентът я докладва като единична булева стойност.
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;
Една рутина само за четене, която записва отчет
Сглобете частите заедно и одитът се превръща в една процедура, която зарежда документ, изброява неговите скриптове и техните тела, изброява неговите цели на Launch и URI, докладва MDP нивото на подписа, отбелязва прикачените файлове и XFA и записва констатациите в журнал. Тя не рендерира нищо, така че е евтина и не може да бъде подведена да покаже враждебно съдържание на страницата. Изходът е плосък, четим от човек запис, по който рецензентът или последващо правило могат да действат.
Формата, която работи добре на практика, е да се събира всяка констатация как ред, да се поставя префикс на истински рисковите, така че да се сортират в горната част на опашката за преглед, и да се запази всичко това до файла. Документ без скриптове, без действия за стартиране, без прикачени файлове, без XFA и без подпис или с последователно сертифициране преминава безшумно. Документ, който задейства няколко флага едновременно, е този, който човек трябва да види, преди следващ етап да го отвори. Одитът не взема решение за доверие вместо вас. Той се грижи решението да бъде информирано, а не сляпо.
След като даден файл премине одита и трябва да го прегледате, направете го под ограничение, а не в четец по подразбиране. Подходът в нашето ръководство за изграждане на безопасен предварителен преглед на PDF в Delphi показва как да се попречи на автоматичната обработка на връзки и активното съдържание да действат по време на контролиран преглед. За да сгънете това изброяване в пълен конвейер за приемане с инструменти за рецензиране, вижте статията за приемане на PDF и работна маса за рецензиране. И двете се изграждат върху една и съща основа само за четене, без рендериране, и се доставят като част от PDFium Component за Delphi и C++Builder, заедно с API за рендериране, текст, форми и подписи, разгледани на други места в този блог.