Технічна стаття

Безпечне повторення недовірених EMF і WMF у HotXLS

Книга Excel може нести зображення EMF і WMF, і звичайний спосіб намалювати одне — віддати потік байт системному програвачу метафайлів. Те рішення варте прямого погляду: метафайл — це серіалізований потік команд для графічного API, і відтворити його означає дозволити файлу, що прибув поштою, керувати графічним драйвером. HotXLS бере інший маршрут. XLSDecodeVectorScene сама розбирає метафайл, перевіряє заголовок, кожен розмір запису, заявлений загальний підрахунок записів і точне розміщення запису кінця файла, беззастережно відкидає escape-записи і повертає TXLSVectorScene з примітивних команд малювання, які бекенди Canvas та SVG повторюють власним кодом. Жодного відтворення драйвером немає в жодний момент

HotXLS розбирає недовірені байти EMF і WMF робочого аркуша через XLSDecodeVectorScene у список команд TXLSVectorScene замість відтворення метафайла GDI
HotXLS сама розбирає метафайл і повертає примітивні команди для повторення Canvas і SVG; звичайний маршрут виконує потік байт на графічному стеку

Той обмін — покриття заради стримання. Білий список команд, орієнтованих на прямокутники, не відтворить кожен метафайл, який може творити дизайнер, тож сцена звітує, скільки записів малювання вона не змогла подати, а викликач вирішує, що з цим робити. Для серверного процесу, що рендерить документи, яких він не творив, той обмін повернений правильним боком

Чому відтворення метафайла погано пасує до недовірених входів?

Бо формат — не картина, а програма. Потік записів EMF маніпулює стеком стану контексту пристрою, розподіляє та вибирає об'єкти з таблиці дескрипторів і може нести escape-записи, чиє корисне навантаження передається драйверу пристрою. Відтворення його вправляє шляхи в графічному стеку платформи, написані з припущення, що метафайл прийшов від співпрацюючого застосунку на тій самій машині. Коли вхід — вкладення електронної таблиці, те припущення зникло, і жодна обережність усередині бібліотеки таблиць не допоможе, бо бібліотека — не той компонент, що розбирає

Це та сама логіка, що керує шаром контейнерів. Книга — це архів ZIP, і HotXLS перевіряє його центральний каталог, а не довіряє заявленим зміщенням, як описано в статті про перевірку ZIP end-of-central-directory. Корисні навантаження метафайлів — наступний шар тієї самої проблеми

Що декодер перевіряє, перш ніж що-небудь намалювати

Перевірка структурна і відбувається завчасно, бо парсер, що починає малювати і перевіряє на ходу, уже діяв на даних, які не звірив. Заголовок мусить збігатися строго, а не правдоподібно. Кожен запис мусить заявляти розмір, що вміщується в решту буфера і достатній для власних фіксованих полів. Підрахунок записів, який заявляє заголовок, мусить збігатися з записами, що справді присутні. Запис кінця файла мусить сидіти точно там, де потік скінчився, а не просто десь поруч, — це закриває трюк із хвостовим сміттям, що ховає друге навантаження за чинною картиною

Понад структуру декодер відмовляє безпечно на семантиці. Escape-записи відкидаються, а не пропускаються. Запис, що змінює стан і який декодер не моделює, робить декодування невдалим, а не ігнорується, бо ігнорування зміни стану означає: кожна наступна команда малювання виконується в стані, якого файл не просив, і результат — картина, неправильна так, як ніхто не передбачить. Записи малювання поза підтримуваним набором команд — інша справа: їх рахують і пропускають, бо відсутня фігура — видима, звітована прогалина, а не мовчазне пошкодження

XLSDecodeVectorScene перевіряє завчасно заголовок, розміри записів, підсумки та розміщення EOF, потім відкидає escape-записи і рахує непідтримувані записи малювання
Структурні перевірки йдуть завчасно, безпечна семантика відмови відкидає escape-записи, а непідтримувані записи малювання лише рахуються та пропускаються

Бюджети — частина контракту формату

Векторні формати мають власну версію бомби розстискання. Кілька кілобайт записів можуть заявити полівінії зі сотнями мільйонів точок чи зображення, чиї заявлені розміри перемножуються в терабайти. Межі тому мусять бути явними константами, а не тим, що машина випадково пережила

// З lxVectorScene: бюджет декодування, заявлений явно, а не приховано
XL_VECTOR_MAX_RECORDS           = 1000000;
XL_VECTOR_MAX_HANDLES           = 4096;
XL_VECTOR_MAX_DC_DEPTH          = 32;
XL_VECTOR_MAX_COMMANDS          = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS      = 2000000;
XL_VECTOR_MAX_TEXT_CHARS        = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS  = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE        = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS      = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES       = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD             = 1000000000;

Дві з них заслуговують примітки. Стеля глибини контексту пристрою 32 існує, бо записи SaveDC та RestoreDC гніздяться, і неврівноважений потік може штовхати вічно; 32 щедро для справжніх метафайлів і дешево в примусі. Стеля координат існує, бо координати живлять трансформацію, і значення біля меж діапазону цілих дає перетворений результат, що або нескінченний, або згортається, після чого кожне обчислення обвідної рамки внизу за течією — нісенітниця. Обмеження координат під час розбору набагато легше осмислити, ніж захищати кожного споживача геометрії

Константи бюджету декодування в HotXLS lxVectorScene для записів, дескрипторів, глибини DC, команд, точок, тексту, розміру зображення та обмеження координат
Кожна межа — іменована константа, яку примушують під час розбору; стеля глибини DC та обмежувач координат заслуговують найбільшої уваги

Користування сценою

Декодер віддає об'єкт, яким ви володієте, кількість команд, номінальний розмір і кількість записів малювання, які він вирішив не подавати

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data тримає сире навантаження картинки, взяте з книги
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // Відкинуто: заголовок, межі, підсумки, розміщення EOF чи бюджет
    LogReject('metafile rejected: ' + Error);
    Exit;
  end;
  try
    if Scene.SkippedDrawRecords > 0 then
      LogWarning(Format('%d drawing records outside the safe subset',
        [Scene.SkippedDrawRecords]));
    for I := 0 to Scene.Count - 1 do
      case Scene.Commands[I].Kind of
        xlsvcRectangle: DrawRect(Scene.Commands[I]);
        xlsvcEllipse:   DrawEllipse(Scene.Commands[I]);
        xlsvcPolyline,
        xlsvcPolygon,
        xlsvcBezier:    DrawPath(Scene.Commands[I]);
        xlsvcText:      DrawText(Scene.Commands[I]);
        xlsvcImage:     DrawImage(Scene.Commands[I]);
      end;
  finally
    Scene.Free;
  end;
end;

Запис команди несе все, що потрібно бекенду, і нічого, що вимагає пристрою: присутність пера, колір, товщину та стиль; присутність пензля та колір; геометрію; а для тексту — рядок, назву шрифта, розмір, стилі та вирівнювання. Саме це робить ту саму сцену придатною і для екранного рендерера канви, і для автора SVG, і тому векторний шлях не розходиться між переглядом та експортом. Екранний рендеринг вмісту аркуша загалом охоплено в статті про власний рендеринг сітки VCL

Відкидання картинки не пошкоджує книгу

Важлива властивість цієї конструкції: відкинуте декодування стосується лише рендерингу. Оригінальне навантаження лишається в моделі, тож книга, яку відкрили й знову зберегли, виносить свої зображення метафайлів байт у байт, чи міг безпечний декодер їх намалювати, чи ні. Наявний обмежений растровий шлях також лишається доступним як резерв. Інакше кажучи, строгий парсер перепоняє тому, що виконується, а не тому, що зберігається, — та відмінність дозволяє випустити зміну, мотивовану безпекою, не перетворивши її на зміну, що втрачає дані

Обробку об'єктів малювання загалом, включно з частинами об'єктної моделі, що виживають у туди й назад недоторканими, охоплено в статті про діаграми, зображення та малюнки

Де це лишає серверне розгортання

Якщо ви рендерите завантажені користувачами книги в службі, практична позиція тепер оборонна: зображення метафайлів розбирає код, який ви можете перевірити, обмежений константами, які ви можете прочитати, і жодного разу не віддається графічному драйверу. Чесне застереження — покриття. Складні метафайли від інструментів малювання впратимуть на лічильник пропущених записів, і відповідь — винести лічильник на поверхню, а не тихо розширювати білий список. Картинка, що рендериться частково і каже про це, — розмова підтримки; картинка, що рендериться неправильно і мовчить, — звіт про ваду від клієнта

HotXLS обробляє XLS, XLSX, ODS та CSV власними засобами в Delphi та C++Builder без уставленого Excel, і та сама філософія обмеженого розбору проходить крізь його шари контейнерів, формул та малюнків. Деталі формату та безпеки наведені на сторінці продукту HotXLS Delphi spreadsheet component