Artigo Técnico

Replay seguro de EMF e WMF não confiáveis no HotXLS

Uma pasta de trabalho do Excel pode carregar imagens EMF e WMF, e o jeito convencional de desenhar uma é entregar o stream de bytes ao player de metafile do sistema operacional. Essa é uma decisão que vale olhar de frente: um metafile é um stream de comandos serializado para uma API de gráficos, e reproduzi-lo significa deixar um arquivo que chegou por e-mail dirigir o driver de gráficos. O HotXLS toma o outro caminho. O XLSDecodeVectorScene faz o parse do metafile ele mesmo, valida o header, cada tamanho de registro, o total de registros declarado e a colocação exata do registro de fim de arquivo, rejeita registros escape de plano e retorna uma TXLSVectorScene de comandos de desenho primitivos que os backends Canvas e SVG reproduzem pelo próprio código deles. Nenhum playback de driver está envolvido em nenhum ponto

O HotXLS faz o parse de bytes EMF e WMF não confiáveis de planilhas com XLSDecodeVectorScene em uma lista de comandos TXLSVectorScene em vez de playback GDI de metafile
O HotXLS faz o parse do metafile ele mesmo e retorna comandos primitivos para reprodução em Canvas e SVG; a rota convencional executa o stream de bytes na pilha de gráficos

A troca é cobertura por contenção. Uma whitelist de comandos orientada a retângulos não reproduzirá todo metafile que um designer possa criar, então a cena reporta quantos registros de desenho não conseguiu representar e o chamador decide o que fazer com isso. Para um processo servidor que renderiza documentos que não criou, essa troca está do jeito certo

Por que o playback de metafile é um mau encaixe para entrada não confiável?

Porque o formato não é uma imagem, é um programa. Um stream de registros EMF manipula uma pilha de estado de device context, aloca e seleciona objetos de uma tabela de handles, e pode carregar registros escape cujo payload é repassado a um driver de dispositivo. Reproduzi-lo exercita caminhos na pilha de gráficos da plataforma escritos sob a suposição de que o metafile veio de um aplicativo cooperante na mesma máquina. Quando a entrada é um anexo de planilha, essa suposição se foi, e nenhum cuidado dentro da biblioteca de planilhas ajuda porque a biblioteca não é o componente fazendo o parse

Este é o mesmo raciocínio que rege a camada de contêiner. Uma pasta de trabalho é um arquivo ZIP, e o HotXLS valida seu central directory em vez de confiar em offsets declarados, como descrito em o artigo de validação do ZIP end-of-central-directory. Payloads de metafile são a próxima camada do mesmo problema

O que o decoder confere antes de desenhar qualquer coisa

A validação é estrutural e acontece de antemão, porque um parser que começa a desenhar e valida conforme avança já agiu sobre dados que não verificou. O header precisa bater estritamente, não plausivelmente. Cada registro precisa declarar um tamanho que caiba dentro do buffer restante e seja grande o suficiente para seus próprios campos fixos. A contagem de registros que o header declara precisa bater com os registros realmente presentes. O registro de fim de arquivo precisa estar exatamente onde o stream termina, não meramente em algum lugar perto, o que fecha o truque do lixo ao final que esconde um segundo payload atrás de uma imagem válida

Além da estrutura, o decoder é fail-closed na semântica. Registros escape são recusados, não pulados. Um registro que muda estado e que o decoder não modela faz o decode falhar em vez de ser ignorado, porque ignorar uma mudança de estado significa que todo comando de desenho subsequente é executado em um estado que o arquivo não pediu, e o resultado é uma imagem errada de um jeito que ninguém consegue prever. Registros de desenho fora do conjunto de comandos suportado são outra história: esses são contados e pulados, porque uma forma ausente é uma lacuna visível e reportável em vez de uma corrupção silenciosa

XLSDecodeVectorScene confere header, tamanhos de registros, totais e colocação do EOF de antemão, depois recusa registros escape e conta registros de desenho não suportados
Verificações estruturais rodam de antemão e a semântica fail-closed recusa registros escape, enquanto registros de desenho não suportados são apenas contados e pulados

Orçamentos são parte do contrato do formato

Formatos vetoriais têm sua própria versão da bomba de descompressão. Poucos kilobytes de registros podem declarar polylines com centenas de milhões de pontos, ou uma imagem cujas dimensões declaradas multiplicam em terabytes. Os limites portanto precisam ser constantes explícitas em vez de o que a máquina por acaso aguentar

// De lxVectorScene: o orçamento de decode, enunciado em vez de implícito
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;

Dois deles merecem uma nota. O teto de profundidade de device context de 32 existe porque registros SaveDC e RestoreDC se aninham, e um stream desbalanceado pode empilhar para sempre; 32 é generoso para metafiles reais e barato de aplicar. O teto de coordenadas existe porque coordenadas alimentam uma transformação, e um valor perto dos limites do intervalo de inteiros produz um resultado transformado que é infinito ou dá wrap, depois do qual toda computação de bounding box a jusante é absurdo. Limitar coordenadas em tempo de parse é muito mais fácil de raciocinar do que defender todo consumidor da geometria

Constantes de orçamento de decode no lxVectorScene do HotXLS para registros, handles, profundidade de DC, comandos, pontos, texto, tamanho de imagem e limitação de coordenadas
Cada limite é uma constante nomeada aplicada durante o parse; o teto de profundidade de DC e a limitação de coordenadas merecem a maior atenção

Usando a cena

O decoder devolve um objeto que você possui, uma contagem de comandos, um tamanho nominal e uma contagem de registros de desenho que escolheu não representar

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data contém o payload bruto da imagem tirado da pasta de trabalho
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // Recusado: header, limites, totais, colocação do EOF ou um orçamento
    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;

O record de comando carrega tudo de que um backend precisa e nada que exija um dispositivo: presença de caneta, cor, largura e estilo; presença e cor de brush; a geometria; e para texto a string, nome da fonte, tamanho, estilos e alinhamento. É isso que torna a mesma cena utilizável tanto pelo renderizador de canvas na tela quanto pelo escritor SVG, e é por isso que o caminho vetorial não diverge entre preview e exportação. A renderização em tela de conteúdo de planilhas em geral é coberta em o artigo de renderização de grid VCL customizado

Recusar uma imagem não danifica a pasta de trabalho

Uma propriedade importante deste design é que um decode recusado afeta apenas a renderização. O payload original permanece no modelo, então uma pasta de trabalho aberta e salva de novo carrega suas imagens metafile para fora byte a byte, quer o decoder seguro conseguisse desenhá-las ou não. O caminho raster limitado existente também continua disponível como fallback. Em outras palavras, o parser estrito porta o gate no que é executado, não no que é preservado, que é a distinção que permite a uma mudança motivada por segurança ser lançada sem virar uma mudança de perda de dados

O tratamento de objetos de desenho em geral, incluindo as partes do modelo de objetos que sobrevivem intocadas a idas e voltas, é coberto em o artigo de gráficos, imagens e desenhos

Onde isso deixa uma implantação em servidor

Se você renderiza pastas de trabalho enviadas por usuários em um serviço, a posição prática agora é defensável: imagens metafile são interpretadas por código que você pode auditar, limitadas por constantes que você pode ler e nunca entregues a um driver de gráficos. A ressalva honesta é a cobertura. Metafiles complexos produzidos por ferramentas de desenho vão bater no contador de registros pulados, e a resposta é expor o contador em vez de alargar a whitelist em silêncio. Uma imagem que renderiza parcialmente e diz isso é uma conversa de suporte; uma imagem que renderiza errado e não diz nada é um relatório de bug de um cliente

O HotXLS lida com XLS, XLSX, ODS e CSV nativamente em Delphi e C++Builder sem Excel instalado, e a mesma filosofia de parse limitado atravessa suas camadas de contêiner, fórmula e desenho. Detalhes de formato e segurança estão listados na página de produto do HotXLS Delphi spreadsheet component