Artigo Técnico

Reproduzir EMF e WMF não confiáveis com segurança no HotXLS

Um livro Excel pode transportar imagens EMF e WMF, e a forma convencional de desenhar uma é entregar o stream de bytes ao reprodutor de metaficheiros do sistema operativo. Essa é uma decisão que vale a pena olhar diretamente: um metaficheiro é um stream de comandos serializado para uma API de gráficos, e reproduzi-lo significa deixar um ficheiro que chegou por email comandar o driver gráfico. O HotXLS toma o outro caminho. O XLSDecodeVectorScene analisa ele próprio o metaficheiro, valida o cabeçalho, cada tamanho de registo, o total de registos declarado e a colocação exata do registo de fim de ficheiro, recusa registos escape de imediato, e devolve uma TXLSVectorScene de comandos de desenho primitivos que os backends Canvas e SVG reproduzem através do seu próprio código. Nenhuma reprodução por driver está envolvida em ponto algum

O HotXLS analisa bytes EMF e WMF não confiáveis de folhas de cálculo com XLSDecodeVectorScene numa lista de comandos TXLSVectorScene em vez de reprodução de metaficheiros GDI
O HotXLS analisa ele próprio o metaficheiro e devolve comandos primitivos para reprodução 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á todos os metaficheiros que um designer pode criar, pelo que a cena reporta quantos registos de desenho não conseguiu representar e o chamador decide o que fazer com isso. Para um processo de servidor que renderiza documentos que não criou, essa troca está do lado certo

Porque é que a reprodução de metaficheiros serve mal a entrada não confiável?

Porque o formato não é uma imagem, é um programa. Um stream de registos EMF manipula uma pilha de estado de contexto de dispositivo, aloca e seleciona objetos de uma tabela de handles, e pode transportar registos escape cujo conteúdo é passado a um driver de dispositivo. Reproduzi-lo exercita caminhos na pilha de gráficos da plataforma que foram escritos partindo do pressuposto de que o metaficheiro veio de uma aplicação cooperante na mesma máquina. Quando a entrada é um anexo de folha de cálculo, esse pressuposto desaparece, e nenhum cuidado dentro da biblioteca de folhas de cálculo ajuda porque a biblioteca não é o componente que faz a análise

Este é o mesmo raciocínio que rege a camada de contentores. Um livro é um arquivo ZIP, e o HotXLS valida o seu diretório central em vez de confiar nos desvios declarados, como descrito no artigo sobre validação do end-of-central-directory ZIP. Os conteúdos de metaficheiros são a camada seguinte do mesmo problema

O que o descodificador verifica antes de desenhar alguma coisa

A validação é estrutural e acontece logo no início, porque um parser que começa a desenhar e valida ao mesmo tempo já atuou sobre dados que não verificou. O cabeçalho tem de corresponder estritamente e não apenas plausivelmente. Cada registo tem de declarar um tamanho que caiba dentro do buffer restante e seja suficientemente grande para os seus campos fixos. A contagem de registos que o cabeçalho declara tem de corresponder aos registos realmente presentes. O registo de fim de ficheiro tem de estar exatamente onde o stream termina, não meramente algures perto, o que fecha o truque do lixo final que esconde uma segunda carga atrás de uma imagem válida

Para além da estrutura, o descodificador falha em modo fechado na semântica. Registos escape são recusados, não saltados. Um registo que mude o estado e que o descodificador não modele faz a descodificação falhar em vez de ser ignorado, porque ignorar uma mudança de estado significa que cada comando de desenho subsequente é executado num estado que o ficheiro não pediu, e o resultado é uma imagem errada de um modo que ninguém pode prever. Registos de desenho fora do conjunto de comandos suportado são outra história: esses são contados e saltados, porque uma forma em falta é um buraco visível e reportável em vez de uma corrupção silenciosa

O XLSDecodeVectorScene verifica o cabeçalho, tamanhos de registos, totais e colocação do EOF logo no início, depois recusa registos escape e conta registos de desenho não suportados
As verificações estruturais correm logo no início e a semântica de falha fechada recusa registos escape, enquanto os registos de desenho não suportados são só contados e saltados

Os orçamentos fazem parte do contrato do formato

Os formatos vetoriais têm a sua própria versão da bomba de descompressão. Alguns quilobytes de registos podem declarar polilinhas com centenas de milhões de pontos, ou uma imagem cujas dimensões declaradas multiplicam para terabytes. Os limites portanto têm de ser constantes explícitas em vez do que a máquina por acaso sobreviva

// De lxVectorScene: o orçamento de descodificação, 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;

Duas destas merecem uma nota. O limite de profundidade de contexto de dispositivo de 32 existe porque os registos SaveDC e RestoreDC aninham-se, e um stream desequilibrado pode empilhar para sempre; 32 é generoso para metaficheiros reais e barato de impor. O limite de coordenadas existe porque as coordenadas alimentam uma transformação, e um valor perto dos limites do intervalo de inteiros produz um resultado transformado que é infinito ou dá a volta, depois do que todos os cálculos de bounding box a jusante são disparates. Limitar as coordenadas em tempo de análise é muito mais fácil de raciocinar do que defender todos os consumidores da geometria

Constantes de orçamento de descodificação no lxVectorScene do HotXLS para registos, handles, profundidade de DC, comandos, pontos, texto, tamanho de imagem e limitação de coordenadas
Todos os limites são constantes nomeadas impostas durante a análise; o limite de profundidade de DC e a limitação de coordenadas merecem mais atenção

Usar a cena

O descodificador entrega um objeto que é seu, uma contagem de comandos, um tamanho nominal, e uma contagem de registos de desenho que escolheu não representar

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data guarda o conteúdo bruto da imagem retirado do livro
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // Recusado: cabeçalho, 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 registo de comando transporta tudo o que um backend precisa e nada que exija um dispositivo: presença de caneta, cor, largura e estilo; presença e cor de pincel; a geometria; e para texto a string, nome de fonte, tamanho, estilos e alinhamento. É isso que torna a mesma cena utilizável tanto pelo renderizador de canvas no ecrã como pelo escritor SVG, e é por isso que o caminho vetorial não diverge entre pré-visualização e exportação. A renderização no ecrã de conteúdo de folhas de cálculo em geral está coberta no artigo sobre renderização de grelha VCL personalizada

Rejeitar uma imagem não danifica o livro

Uma propriedade importante deste design é que uma descodificação recusada afeta apenas a renderização. O conteúdo original fica no modelo, pelo que um livro que é aberto e gravado de novo transporta as suas imagens de metaficheiro para fora byte a byte, quer o descodificador seguro as consiga desenhar quer não. O caminho raster limitado existente também continua disponível como recurso. Por outras palavras, o parser estrito condiciona o que é executado, não o que é preservado, que é a distinção que permite uma alteração motivada por segurança ser lançada sem se transformar numa alteração de perda de dados

O tratamento de objetos de desenho em geral, incluindo as partes do modelo de objetos que sobrevivem intactas às viagens de ida e volta, está coberto no artigo sobre gráficos, imagens e desenhos

Onde isto deixa uma implantação em servidor

Se renderiza livros carregados por utilizadores num serviço, a posição prática agora é defensável: as imagens de metaficheiro são analisadas por código que pode auditar, limitadas por constantes que pode ler, e nunca entregues a um driver gráfico. A ressalva honesta é a cobertura. Metaficheiros complexos produzidos por ferramentas de desenho vão ativar o contador de registos saltados, e a resposta é evidenciar o contador em vez de alargar a whitelist em silêncio. Uma imagem que renderiza parcialmente e o diz é uma conversa de suporte; uma imagem que renderiza mal e não diz nada é um relatório de bug de um cliente

O HotXLS trata XLS, XLSX, ODS e CSV nativamente em Delphi e C++Builder sem Excel instalado, e a mesma filosofia de análise limitada atravessa as suas camadas de contentor, fórmulas e desenhos. Os detalhes de formato e segurança estão listados na página de produto do HotXLS Delphi spreadsheet component