Техническа статия

Изолирайте PDF кодеци в worker процеси с HotPDF

HotPDF може да декодира трите най-рискови PDF филтъра за изображения — DCTDecode, JPXDecode и JBIG2Decode — вътре в отделен, краткотраен worker процес, вместо във вашето приложение. Свойството, което включва това, е CodecIsolationMode, а практическият ефект е, че деформиран JPEG 2000 codestream, който преди би съборил вашето VCL приложение, сега убива изхвърлящ се дъщерен процес, докато хостът докладва статус код и продължава напред

Тази разлика има най-голямо значение точно там, откъдето реално пристигат PDF файловете: форма за качване, пощенски шлюз, скенер, партньорска FTP папка. Не контролирате тези байтове, а точно кодеците за изображения са мястото, където живее историческата вреда

Защо едно лошо изображение сваля цялото приложение?

Защото кодекът за изображения е онази част от PDF четец, която изпълнява сложна машина от състояния върху данни, контролирани от атакуващ, почти без структурни проверки, на които да разчита. Докато байтовете достигнат JPEG 2000 или JBIG2 декодера, cross-reference таблицата вече е разчетена, обектът е разрешен, веригата от филтри е разгъната, а онова, което остава, е суров codestream, който казва колко тайла, колко компонента, колко бита на сампъл. Грешно число там не е грешка в парсването. Това е неправилен размер на заделяне или индекс извън диапазона вътре в стегнат декодиращ цикъл

Бюджетните лимити помагат и трябва вече да ги имате. HotPDF ограничава разширяването с DecodeBudgetBytes и DocumentDecodeBudgetBytes и ограничава веригите от филтри с DecodeFilterLimit и DecodePipelineDepthLimit; разсъждението зад тези лимити е разгледано в ограниченото декодиране за вложени филтри и PDF бомби. Но байтов бюджет отговаря само на един въпрос — колко изход е позволен. Той не може да отговори какво се случва, когато декодерът се провали, преди да е произвел изобщо изход. Access violation вътре в декодиращ цикъл не е нарушение на политика, което можете да отхвърлите; това е събитие на ниво процес, а единствената надеждна защита срещу такова събитие е различен процес

Какво изолира HotPDF и какво не

HotPDF изолира точно три вида кодеци, изброени като hckDCT, hckJPX и hckJBIG2 в модула HPDFCodecIsolation. Всичко останало — Flate, LZW, RunLength, ASCII85, CCITT — остава в процеса, защото тези декодери са достатъчно прости, за да бъдат ограничени с бюджети, и не са мястото, откъдето идват интересните провали

Транспортният слой е съзнателно тесен. Хостът заделя едно ограничено shared-memory картографиране, записва фиксиран THPDFCodecSharedHeader плюс компресирания вход и всякакви JBIG2 глобални сегменти, стартира worker-а и чака. Worker-ът записва декодираните пиксели обратно в същото картографиране и задава статусна дума. Няма протокол с pipe-ове, който да излезе от синхрон, няма формат за сериализация за fuzz-ване, а заглавната част носи магическа стойност и версия, така че несъответстващ worker binary се отхвърля, вместо да бъде погрешно разчетен

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: никога не декодирай тези кодеци в процеса
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 или >= 64 MiB
    Pdf.DecodeBudgetBytes := 134217728;

    if Pdf.LoadFromFile('untrusted-upload.pdf') = 1 then
      if Pdf.GetLoadedImageCount > 0 then
      begin
        Bmp := Pdf.ExtractLoadedImage(0);
        try
          if Pdf.GetLastCodecWorkerInfo(Info) then
            LogCodecOutcome(Info);
        finally
          Bmp.Free;
        end;
      end;
  finally
    Pdf.Free;
  end;
end;

Оставете CodecWorkerExecutable празно и HotPDF ще открие worker-а до собствения ви изпълним файл, като HotPDFCodecWorker.exe в директорията на ParamStr(0). Задайте го изрично, когато внедряването ви поставя worker-а другаде; стойността се разгъва през ExpandFileName, така че относителен път се разрешава спрямо текущата директория, а не спрямо директорията на приложението, което рядко е желаното при услуга

Automatic или Required: кой провал предпочитате?

Трите стойности на THPDFCodecIsolationMode кодират три различни отговора на един въпрос — какво трябва да се случи, когато worker-ът изобщо не може да се стартира. cimDisabled пропуска изолацията напълно и декодира в процеса, предишното поведение до версия 3.x. cimAutomatic, по подразбиране, опитва worker-а и тихо превключва към декодиране в процеса, когато изпълнимият файл на worker-а липсва или не се стартира, което се докладва като статус cwsUnavailable. cimRequired отказва това превключване: недостъпен worker маркира декодирането като обработено и провалено, така че никой ненадежден codestream не достига до вашето адресно пространство

Изберете според модела на заплаха, не според удобство. Настолен зрител, отварящ документи, които потребителят вече има на диска, е добре с cimAutomatic, където липсващ worker деградира до класическото поведение, вместо да счупи продукта. Услуга за прием, парсваща файлове от интернет, трябва да работи с cimRequired, защото грешка при внедряване, която тихо премахва слоя за изолация, е точно видът регресия, която никой не забелязва, докато не стане проблем. Отбележете асиметрията: само cwsUnavailable задейства превключването. Worker, който се е стартирал и после се е сринал, е timeout-нал или е достигнал лимит, е провал на декодирането и в двата режима, никога тих повторен опит в процеса

Четене на присъдата от THPDFCodecWorkerStatus

GetLastCodecWorkerInfo връща резултата от последното изолирано декодиране, а изброяването на статуси е достатъчно конкретно, за да задвижи реални оперативни решения, а не общ ред „изображението се провали“ в лога. Стойностите са cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError и cwsOutputLimit

Третирайте ги като три групи. Проблеми с внедряването са cwsUnavailable и cwsLaunchFailed: някой е доставил без worker-а, или антивирусен продукт блокира създаването на процеси. Проблеми с документа са cwsDecodeFailed и cwsOutputLimit: файлът е деформиран или по-голям, отколкото политиката ви позволява, а отхвърлянето му е правилният отговор. Интересната група е cwsTimedOut и cwsCrashed, защото това са събитията, които преди биха закачили или убили хост процеса. Когато това се случи, съпътстващите полета ProcessId, ExitCode и ElapsedMilliseconds ви дават достатъчно, за да съпоставите с запис в Windows Error Reporting и да решите дали един файл на клиент е патологичен, или някой ви сондира

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // няма какво да се докладва
    cwsUnavailable, cwsLaunchFailed:
      Alert('Codec worker not deployed: ' + Info.ErrorMessage);
    cwsTimedOut, cwsCrashed:
      Quarantine(Format('pid %d exit %d after %d ms',
        [Info.ProcessId, Info.ExitCode, Info.ElapsedMilliseconds]));
  else
    RejectDocument(Info.ErrorMessage);
  end;
end;

Лимитите, които реално ограничават

Три отделни тавана важат за всяко изолирано декодиране, а знанието кой от тях се е задействал спестява един следобед в гадаене. CodecWorkerTimeoutMilliseconds е по подразбиране 10 000 и се валидира в диапазона от 1 до 600 000; стойност извън него хвърля изключение, вместо тихо да се орязва. CodecWorkerMemoryLimitBytes е по подразбиране 536 870 912 байта и трябва да е или нула, означаваща без лимит, или поне 67 108 864 байта, защото по-малък таван не може да побере реалистичен работен набор на декодер и би провалил всеки документ. Лимитът на паметта се прилага чрез Windows Job Object с семантика kill-on-close, така че worker-ът умира заедно с job-а дори ако хостът бъде прекратен рязко

Третият таван е лимитът на изхода, а той е изведен, не конфигуриран. HotPDF изчислява необходимите байтове от заявения регион или от очакваната геометрия на изображението — като ширина по височина по три за 24-битов изход — после орязва тази стойност до DecodeBudgetBytes, когато е зададен бюджет. Декодер, който докладва правдоподобно заглавие и после се опитва да излъчи далеч повече пиксели, отколкото геометрията позволява, се спира от самото картографиране, а хостът вижда cwsOutputLimit. Точно затова слоят за изолация и бюджетът за декодиране са допълващи се: бюджетът определя колко голямо е позволено да бъде едно изображение, а границата на изолация гарантира, че лъжа за този размер не може да се превърне в out-of-bounds запис във вашия процес

Мястото на този слой в укрепен път за прием

Изолацията на процеси е най-външният слой на защитна верига, която започва много по-рано. Структурните лимити отхвърлят неправдоподобни документи още при парсването. Бюджетите за филтри ограничават разширяването. Изолацията сдържа онова, което оцелее и от двете. За документи, достигащи слоя за изображения, си струва да знаете кой кодек реално се използва, тъй като обработката на JPXDecode и символните речници на JBIG2 имат много различни профили на провал, а JBIG2 в частност носи глобални сегменти през страници, които наивен sandbox по изображение би счупил

Цената е честна и си струва да се каже: стартирането на процес за всяко изолирано изображение добавя милисекунди, а документ със стотици сканирани страници ще го усети. Измерете това спрямо онова, което купувате. При batch конвертор, работещ без надзор през нощта, загубата на пропускателна способност е невидима, а сдържането на срив е целият смисъл. При интерактивен зрител, отварящ документи, на които потребителят вече вярва, cimDisabled или cimAutomatic е разумната стойност по подразбиране. Режимът е обикновено свойство, така че нищо не ви пречи да избирате по клас документ по време на изпълнение

HotPDF доставя слоя за изолация, бюджетите за декодиране и структурните лимити на парсера като един native VCL компонент за Delphi и C++Builder, без външен runtime за внедряване извън самия worker изпълним файл. Пълната API документация и пробна версия са достъпни на страницата на HotPDF PDF компонента за Delphi