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

Безопасное воспроизведение недоверенных 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