Artículo técnico

Reproducir EMF y WMF no confiables con seguridad en HotXLS

Un libro de Excel puede llevar imágenes EMF y WMF, y la forma convencional de dibujar una es entregarle el stream de bytes al reproductor de metarchivos del sistema operativo. Es una decisión que merece mirarse de frente: un metarchivo es un stream de comandos serializado para una API de gráficos, y reproducirlo significa dejar que un fichero que llegó por correo maneje el controlador de gráficos. HotXLS toma la otra ruta. XLSDecodeVectorScene analiza el metarchivo ella misma, valida la cabecera, cada tamaño de registro, el total de registros declarado y la colocación exacta del registro de fin de fichero, rechaza los registros escape sin más, y devuelve una TXLSVectorScene de comandos de dibujo primitivos que los backends Canvas y SVG reproducen con su propio código. En ningún punto interviene reproducción por el controlador

HotXLS analiza bytes EMF y WMF no confiables de la hoja con XLSDecodeVectorScene en una lista de comandos TXLSVectorScene en lugar de reproducción GDI de metarchivo
HotXLS analiza el metarchivo por sí misma y devuelve comandos primitivos para reproducción Canvas y SVG; la ruta convencional ejecuta el stream de bytes en la pila gráfica

El intercambio es cobertura por contención. Una whitelist de comandos orientada a rectángulos no reproducirá todo metarchivo que un diseñador pueda crear, así que la escena informa de cuántos registros de dibujo no pudo representar y quien llama decide qué hacer con ello. Para un proceso servidor que renderiza documentos que no creó, ese intercambio está bien planteado

Por qué la reproducción de metarchivos encaja mal con entrada no confiable?

Porque el formato no es una imagen, es un programa. Un stream de registros EMF manipula una pila de estados de contexto de dispositivo, aloja y selecciona objetos de una tabla de handles, y puede llevar registros escape cuya carga se pasa a un controlador de dispositivo. Reproducirlo ejercita rutas de la pila gráfica de la plataforma escritas suponiendo que el metarchivo venía de una aplicación cooperante en la misma máquina. Cuando la entrada es un adjunto de hoja de cálculo, ese supuesto desaparece, y nada de lo que se cuide dentro de la biblioteca de hojas de cálculo ayuda porque la biblioteca no es el componente que analiza

Es el mismo razonamiento que gobierna la capa de contenedor. Un libro es un archivo ZIP, y HotXLS valida su central directory en lugar de fiarse de los offsets declarados, como se describe en el artículo de validación del end-of-central-directory ZIP. Las cargas de metarchivo son la siguiente capa del mismo problema

Qué comprueba el decodificador antes de dibujar nada

La validación es estructural y ocurre por adelantado, porque un parser que empieza a dibujar y valida sobre la marcha ya ha actuado sobre datos que no ha verificado. La cabecera debe coincidir estrictamente, no plausiblemente. Cada registro debe declarar un tamaño que quepa en el buffer restante y sea bastante grande para sus campos fijos. El recuento de registros que declara la cabecera debe coincidir con los registros realmente presentes. El registro de fin de fichero debe estar exactamente donde termina el stream, no meramente en algún sitio cercano, lo que cierra el truco de la basura final que esconde una segunda carga detrás de una imagen válida

Más allá de la estructura, el decodificador es fail-closed en la semántica. Los registros escape se rechazan, no se saltan. Un registro que cambia de estado y que el decodificador no modela provoca que la decodificación falle en lugar de ignorarse, porque ignorar un cambio de estado significa que cada comando de dibujo posterior se ejecuta en un estado que el fichero no pidió, y el resultado es una imagen equivocada de una forma que nadie puede predecir. Los registros de dibujo fuera del conjunto de comandos soportado son otra cosa: esos se cuentan y se saltan, porque una forma ausente es un hueco visible y reportable, no una corrupción silenciosa

XLSDecodeVectorScene comprueba cabecera, tamaños de registro, totales y colocación del EOF por adelantado, luego rechaza registros escape y cuenta registros de dibujo no soportados
Las comprobaciones estructurales corren por adelantado y la semántica fail-closed rechaza los registros escape, mientras que los registros de dibujo no soportados solo se cuentan y se saltan

Los presupuestos son parte del contrato del formato

Los formatos vectoriales tienen su propia versión de la bomba de descompresión. Unos pocos kilobytes de registros pueden declarar polilíneas con cientos de millones de puntos, o una imagen cuyas dimensiones declaradas multiplican hasta terabytes. Los límites, por tanto, tienen que ser constantes explícitas y no lo que la máquina aguante por casualidad

// De lxVectorScene: el presupuesto de decodificación, declarado y no 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;

Dos de ellos merecen una nota. El tope de profundidad de contexto de dispositivo de 32 existe porque los registros SaveDC y RestoreDC se anidan, y un stream desequilibrado puede apilar para siempre; 32 es generoso para metarchivos reales y barato de aplicar. El tope de coordenadas existe porque las coordenadas alimentan una transformación, y un valor cerca de los límites del rango de enteros produce un resultado transformado que es infinito o desborda, tras lo cual todo cómputo de bounding box aguas abajo es un sinsentido. Acotar coordenadas en el parseo es mucho más fácil de razonar que defender a cada consumidor de la geometría

Constantes de presupuesto de decodificación en lxVectorScene de HotXLS para registros, handles, profundidad de DC, comandos, puntos, texto, tamaño de imagen y acotado de coordenadas
Cada límite es una constante con nombre aplicada durante el parseo; el tope de profundidad de DC y el acotado de coordenadas merecen la mayor atención

Usar la escena

El decodificador devuelve un objeto que es vuestro, un recuento de comandos, un tamaño nominal y un recuento de registros de dibujo que optó por no representar

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data contiene la carga bruta de imagen tomada del libro
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // Rechazada: cabecera, límites, totales, colocación del EOF o un presupuesto
    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;

El registro de comando lleva todo lo que un backend necesita y nada que exija un dispositivo: presencia, color, anchura y estilo de pluma; presencia y color de brocha; la geometría; y para el texto la cadena, nombre de fuente, tamaño, estilos y alineación. Eso es lo que hace que la misma escena sea utilizable tanto por el renderizador de canvas en pantalla como por el escritor SVG, y es la razón por la que la ruta vectorial no diverge entre previsualización y exportación. El renderizado en pantalla de contenido de hojas de cálculo en general se cubre en el artículo de renderizado con grid VCL personalizado

Rechazar una imagen no daña el libro

Una propiedad importante de este diseño es que una decodificación rechazada afecta solo al renderizado. La carga original se queda en el modelo, así que un libro que se abre y se guarda de nuevo saca sus imágenes de metarchivo byte a byte, quisiera o no el decodificador seguro dibujarlas. La ruta rasterizada acotada existente sigue también disponible como fallback. Dicho de otro modo, el parser estricto condiciona lo que se ejecuta, no lo que se conserva, que es la distinción que permite que un cambio motivado por seguridad se publique sin convertirse en un cambio de pérdida de datos

El manejo de objetos de dibujo en general, incluidas las partes del modelo de objetos que sobreviven intactas a los viajes de ida y vuelta, se cubre en el artículo de gráficos, imágenes y dibujos

Dónde deja esto a un despliegue servidor

Si renderizáis libros subidos por usuarios en un servicio, la posición práctica ahora es defendible: las imágenes de metarchivo las analiza código que podéis auditar, las acotan constantes que podéis leer, y nunca se entregan a un controlador de gráficos. La advertencia honesta es la cobertura. Los metarchivos complejos producidos por herramientas de dibujo tocarán el contador de registros saltados, y la respuesta a eso es mostrar el contador en lugar de ensanchar la whitelist en silencio. Una imagen que se renderiza parcialmente y lo dice es una conversación de soporte; una imagen que se renderiza mal y no dice nada es un informe de error de un cliente

HotXLS maneja XLS, XLSX, ODS y CSV de forma nativa en Delphi y C++Builder sin Excel instalado, y la misma filosofía de parseo acotado atraviesa sus capas de contenedor, fórmulas y dibujos. Los detalles de formato y seguridad están en la página de producto de HotXLS Delphi spreadsheet component