HotPDF може декодувати три найризикованіші фільтри зображень PDF — DCTDecode, JPXDecode і JBIG2Decode — в окремому короткоживучому процесі-виконавці замість того, щоб робити це всередині вашого застосунку. Властивість, яка це вмикає, — CodecIsolationMode, а практичний ефект такий: пошкоджений кодовий потік JPEG 2000, який раніше обвалив би ваш VCL-застосунок, тепер убиває одноразовий дочірній процес, а хост-процес фіксує код статусу і продовжує роботу
Ця різниця має найбільше значення саме там, звідки насправді надходять PDF: форма завантаження, поштовий шлюз, сканувальний пристрій, FTP-приймальня партнера. Ви не контролюєте ці байти, а саме в кодеках зображень історично й криється основна шкода
Чому одне погане зображення обвалює весь застосунок?
Тому що кодек зображення — це та єдина частина читача PDF, яка виконує складну машину станів над даними, контрольованими атакуючою стороною, майже без структурних перевірок, на які можна було б покластися. До того моменту, коли байти доходять до декодера JPEG 2000 чи JBIG2, таблиця перехресних посилань уже розібрана, об'єкт уже розв'язано, ланцюжок фільтрів уже розкручено, і залишається лише сирий кодовий потік, який повідомляє, скільки плиток, скільки компонентів, скільки бітів на семпл. Неправильне число тут — це не помилка розбору. Це неправильний розмір виділення пам'яті або індекс поза межами масиву всередині щільного циклу декодування
Обмеження бюджету допомагають, і у вас вони вже мали б бути. HotPDF обмежує розширення через DecodeBudgetBytes і DocumentDecodeBudgetBytes, а ланцюжки фільтрів обмежує через DecodeFilterLimit і DecodePipelineDepthLimit; логіка цих обмежень розглянута в статті про обмежене декодування для вкладених фільтрів і PDF-бомб. Але байтовий бюджет відповідає лише на одне питання — скільки виводу дозволено. Він не може відповісти, що станеться, коли декодер дає збій ще до того, як взагалі створить якийсь вивід. Порушення доступу до пам'яті всередині циклу декодування — це не порушення політики, яке можна просто відхилити; це подія на рівні процесу, а єдине надійне стримування для події на рівні процесу — це інший процес
Що HotPDF ізолює, а що ні
HotPDF ізолює рівно три типи кодеків, перелічені як hckDCT, hckJPX і hckJBIG2 у модулі HPDFCodecIsolation. Усе інше — Flate, LZW, RunLength, ASCII85, CCITT — залишається всередині процесу, бо ці декодери достатньо прості, щоб обмежити їх бюджетами, і саме там цікавих збоїв не буває
Транспорт навмисно вузький. Хост виділяє одне обмежене відображення спільної пам'яті, записує фіксований THPDFCodecSharedHeader разом зі стисненим вводом і будь-якими глобальними сегментами JBIG2, запускає виконавця й чекає. Виконавець записує декодовані пікселі назад у те саме відображення й встановлює слово статусу. Тут немає протоколу каналів, який може розсинхронізуватися, немає формату серіалізації, який можна фазити, а заголовок несе магічне значення й версію, тож невідповідний бінарник виконавця буде відхилено, а не неправильно прочитано
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 МіБ
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 знайде виконавця поруч із власним виконуваним файлом, як HotPDFCodecWorker.exe в каталозі ParamStr(0). Задавайте його явно, коли ваше розгортання розміщує виконавця десь інде; значення розгортається через ExpandFileName, тож відносний шлях розв'язується відносно поточного каталогу, а не каталогу застосунку, що на службі рідко буває тим, чого ви хочете
Автоматично чи обов'язково: якому збою ви віддаєте перевагу?
Три значення THPDFCodecIsolationMode кодують три різні відповіді на одне питання: що має статися, коли виконавець узагалі не може запуститися. cimDisabled повністю пропускає ізоляцію й декодує всередині процесу — це поведінка, яка була до версії 3.x. cimAutomatic, значення за замовчуванням, намагається використати виконавця й тихо повертається до декодування всередині процесу, коли виконуваний файл виконавця відсутній або не запускається, що фіксується як статус cwsUnavailable. cimRequired відмовляється від цього резервного варіанту: недоступний виконавець позначає декодування як оброблене і невдале, тож жоден недовірений кодовий потік ніколи не потрапляє у ваш адресний простір
Обирайте за моделлю загроз, а не за зручністю. Десктопний переглядач, що відкриває документи, які користувач уже має на диску, чудово почувається на cimAutomatic, де відсутній виконавець деградує до класичної поведінки, а не ламає продукт. Служба приймання, що розбирає файли з інтернету, має працювати на cimRequired, бо помилка розгортання, яка тихо усуває шар ізоляції, — це саме той тип регресії, яку ніхто не помічає, доки вона не спрацює. Зверніть увагу на асиметрію: лише cwsUnavailable запускає резервний варіант. Виконавець, що запустився й потім впав, вичерпав тайм-аут або сягнув ліміту, — у обох режимах це збій декодування, і ніколи не тиха повторна спроба всередині процесу
Читання вердикту з THPDFCodecWorkerStatus
GetLastCodecWorkerInfo повертає результат останнього ізольованого декодування, і перелічення статусів достатньо конкретне, щоб керувати реальними операційними рішеннями, а не просто рядком журналу на кшталт «зображення не вдалося». Значення такі: cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError і cwsOutputLimit
Розглядайте їх як три групи. Проблеми розгортання — це cwsUnavailable і cwsLaunchFailed: хтось випустив збірку без виконавця, або антивірус блокує створення процесу. Проблеми документа — це 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, тож виконавець гине разом із job'ом, навіть якщо хост завершується різко
Третя стеля — це обмеження виводу, і воно виводиться, а не налаштовується. HotPDF обчислює потрібну кількість байтів із запитаної області або з очікуваної геометрії зображення як ширина, помножена на висоту, помножена на три для 24-бітного виводу, а потім обрізає це значення до DecodeBudgetBytes, якщо бюджет заданий. Декодер, який повідомляє правдоподібний заголовок, а потім намагається видати значно більше пікселів, ніж дозволяє геометрія, зупиняється самим відображенням, і хост бачить cwsOutputLimit. Саме тому шар ізоляції та бюджет декодування доповнюють одне одного: бюджет визначає, наскільки великим дозволено бути зображенню, а межа ізоляції гарантує, що брехня про цей розмір не перетвориться на запис за межами масиву у вашому процесі
Місце цього в укріпленому шляху приймання
Ізоляція процесу — це найзовнішніший шар ланцюга захисту, що починається набагато раніше. Структурні обмеження відхиляють неправдоподібні документи ще на етапі розбору. Бюджети фільтрів обмежують розширення. Ізоляція стримує те, що пережило обидва попередні шари. Для документів, які доходять до шару зображень, варто знати, який саме кодек ви задіюєте, бо обробка JPXDecode і символьні словники JBIG2 мають дуже різні профілі збоїв, а JBIG2 зокрема несе міжсторінкові глобальні сегменти, які наївна пісочниця для окремого зображення зламала б
Варто чесно назвати ціну: запуск процесу для кожного ізольованого зображення додає мілісекунди, і документ із сотнями сканованих сторінок це відчує. Порівнюйте цю ціну з тим, що вона купує. У пакетному конвертері, що працює без нагляду вночі, втрата пропускної здатності непомітна, а стримування збоїв — це і є вся суть. В інтерактивному переглядачі, що відкриває документи, яким користувач уже довіряє, cimDisabled або cimAutomatic — розумне значення за замовчуванням. Режим — це звичайна властивість, тож ніщо не заважає вибирати його для кожного класу документів окремо під час виконання
HotPDF постачає шар ізоляції, бюджети декодування та структурні обмеження розбору як один нативний VCL-компонент для Delphi та C++Builder, без жодного зовнішнього середовища виконання, яке потрібно розгортати, крім самого виконуваного файлу виконавця. Повна документація API та пробна збірка доступні на сторінці HotPDF Delphi PDF component