Artículo técnico

Fórmulas inseguras en HotXLS: frenar CALL y WEBSERVICE

HotXLS se niega a enrutar 20 nombres de fórmula peligrosos, entre ellos CALL, REGISTER.ID, WEBSERVICE y DDE, hacia sus callbacks de funciones de usuario en Delphi a menos que usted lo active. La propiedad AllowUnsafeFormulaCallbacks del libro viene en False, el chequeo corre antes de evaluar cualquier argumento, y una llamada denegada reporta xlfeUnsafeFunctionDenied sin invocar un solo handler

El escenario que hizo necesario esto es mundano. Un servicio acepta archivos XLS o XLSX subidos, los recalcula del lado del servidor y lee de vuelta unos cuantos totales. La aplicación host registró hace años un handler OnUserFunction para un par de funciones de negocio, y en algún momento ese handler le creció una rama catch-all que reenvía todo lo que no reconoce a una tabla de plugins. Nadie del equipo tipeó jamás =WEBSERVICE(...) en una celda. Lo tipeó quien subió el archivo. Conservar esa fórmula intacta a través de abrir, recalcular y guardar es una función de fidelidad de archivo. Dejar que llegue a código host que puede abrir sockets o archivos es una decisión de autorización, y hasta que HotXLS separó las dos cosas, la librería estaba tomando esa decisión en silencio por usted

¿Por qué preservar una fórmula se convirtió en permiso para ejecutarla?

La causa raíz era un único camino de fallback. HotXLS parsea todo nombre de función de Excel que conoce, pero no todo nombre conocido tiene implementación en el motor de cálculo. Los built-ins reconocidos pero sin implementar caían en el mismo fallback de funciones definidas por el usuario que los nombres genuinamente personalizados, así que CALL y REGISTER.ID compartían camino de despacho con su DISCOUNT o REGIONRATE. Los nombres desconocidos como WEBSERVICE o DDE podían igualmente coincidir con una entrada homónima en el registro del libro, el registro del proceso o un event handler. La mecánica de ese fallback está cubierta en cómo HotXLS resuelve funciones personalizadas vía OnUserFunction; el problema era que nada en ese camino preguntaba si el nombre mismo era uno que un host cuerdo debería ejecutar alguna vez

El orden de despacho importa para lo que «desconocido» significa acá. Una llamada que el motor no puede evaluar de forma nativa se ofrece en orden a los bindings léxicos LAMBDA y LET, que el soporte de closures del motor de fórmulas de HotXLS resuelve primero, luego a las funciones locales del libro registradas con RegisterUserFunction, después a las funciones de proceso desde TXLSWorkbook.RegisterGlobalUserFunction, y finalmente a los eventos OnUserFunction y OnUserFunctionEx. Solo cuando todos declinan una función realmente desconocida se vuelve #NAME?. Cada etapa posterior al lookup de lambdas le entrega el control a código suyo, que es exactamente por qué el chequeo de seguridad tiene que sentarse delante de toda la cadena y no dentro de ningún handler en particular

Cómo HotXLS frena los callbacks de fórmulas inseguras: GetValueItemUserFunction verifica el nombre antes de que se construya el arreglo de argumentos o corra cualquier resolver, así que un =WEBSERVICE(AUDIT_TOKEN()) anidado sale con lxErrorUnsafeFunctionDenied y el log de auditoría queda vacío, mientras que las llamadas seguras recorren la cadena desde los bindings LAMBDA y LET hasta OnUserFunction
Solo cuando todas las etapas declinan se vuelve #NAME? una función realmente desconocida, que es por qué el chequeo de seguridad va delante de toda la cadena y no dentro de un handler cualquiera

¿Qué nombres de función bloquea HotXLS por defecto?

XLSFormulaCallbackIsUnsafe en lxCalc.pas lleva un conjunto fijo de denegación con 20 nombres: DDE, CALL, REGISTER, REGISTER.ID, WEBSERVICE, RTD, SQL.REQUEST, EXEC, RUN, CREATE.OBJECT, APP.ACTIVATE, SEND.KEYS, OPEN, SAVE, SAVE.AS, FOPEN, FWRITE, FWRITELN, FCLOSE y FILE.DELETE. Son los nombres que, en Excel o su lenguaje de macros, cargan código nativo, tocan la red, hablan con otros procesos o tocan el sistema de archivos. Antes de comparar, la función recorta los espacios de los bordes, pasa el nombre a mayúsculas y quita un único prefijo _XLFN. o _XLWS., así que un _xlfn.webservice escrito por una versión más nueva de Excel se atrapa igual que la forma pelada. La lista vive en la frontera de la calculadora y no en los parsers Classic, XLSX y ODS, lo que mantiene un único AST, un único stream de tokens BIFF y un único libro convertido comportándose idéntico

Cómo HotXLS normaliza un nombre de función antes de la comparación de callback inseguro: se recortan los espacios, el nombre pasa a mayúsculas y se quita un único prefijo _XLFN. o _XLWS. de modo que _xlfn.webservice se atrapa como la forma pelada, y luego el resultado se compara exactamente contra el conjunto fijo de denegación de 20 nombres en lxCalc.pas
La lista abarca los nombres que cargan código nativo, tocan la red, hablan con otros procesos o tocan el sistema de archivos, desde DDE, CALL y WEBSERVICE hasta FWRITE y FILE.DELETE

Dos aristas conviene conocer antes de confiar en esto. La coincidencia es exacta, así que un handler que usted registre como MYWEBSERVICE no se ve afectado, y al revés, una UDF interna legítima que casualmente se llame OPEN o RUN ahora queda denegada por defecto. El conjunto de denegación tampoco es un sandbox para sus propios handlers. Si su rama catch-all ejecuta nombres de plugins arbitrarios, el gate frena los peligrosos famosos y nada más; el arreglo durable sigue siendo un handler que compare contra una allowlist explícita con SameText y deje Handled en False para todo lo que no le pertenece

¿Por qué el gate debe correr antes de evaluar los argumentos?

Un gate que dispara después de computar los argumentos llega tarde, porque los argumentos mismos pueden llamar a su código. GetValueItemUserFunction verifica el nombre primero y sale con lxErrorUnsafeFunctionDenied antes de construir el arreglo de argumentos, antes de consultar un resolver o cualquiera de los registros, e incluso antes de notar que no hay ningún handler asignado. Ese orden es lo que derrota al caso anidado de abajo, donde la llamada externa igual sería rechazada, pero una UDF interna de apariencia inofensiva dispararía primero y dejaría su efecto secundario

procedure TImportService.HandleUdf(Sender: TObject;
  const FunctionName: WideString; const Args: Variant;
  var Value: Variant; var Handled: Boolean);
begin
  if SameText(FunctionName, 'AUDIT_TOKEN') then
  begin
    FAuditLog.Add('AUDIT_TOKEN evaluated');   // efecto secundario en código host
    Value := 'token-42';
    Handled := True;
  end;
end;

Book.OnUserFunction := HandleUdf;
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106, y FAuditLog sigue vacío

Default del libro contra TXLSFormulaEvaluationOptions por llamada

La bandera del libro es el default y la opción por llamada tiene la última palabra. TXLSWorkbook.AllowUnsafeFormulaCallbacks y TXLSXWorkbook.AllowUnsafeFormulaCallbacks gobiernan el recálculo ordinario, Calculate, el EvaluateFormulaAt de dos argumentos, las plantillas de evaluación, las vistas de solo lectura y, en XLSX, cada worker del pool de recálculo paralelo. Cualquier punto de entrada que acepte un record TXLSFormulaEvaluationOptions explícito toma Options.AllowUnsafeFormulaCallbacks como el veredicto para esa llamada y no lo mezcla con OR junto a la propiedad del libro. Esa asimetría es deliberada: un trabajo interno de confianza puede autorizar un lookup RTD sin voltear el libro entero, y un libro globalmente habilitado puede igual forzar una evaluación sensible de vuelta a denegar

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // el libro queda cerrado, una llamada de confianza pasa
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // el libro está habilitado, pero esta evaluación de texto subido no
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // la bandera vuelve a False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Cambiar la propiedad del libro también marca el grafo de dependencias como sucio en ambos motores. Sin ese paso, un resultado cacheado computado mientras los callbacks estaban permitidos podría servirse después de que los revocaran, o un desenlace cacheado xlfeUnsafeFunctionDenied podría sobrevivir a una activación. El nuevo estado se agregó al final de TXLSFormulaEvaluationStatus después de xlfeFailed, así que tiene ordinal 10 y cada ordinal existente conserva su valor; la misma regla de agregar al final aplica al campo del record de opciones y al getter y setter de IXLSWorkbook, aunque un consumidor construido contra una versión anterior igual necesita recompilar

¿Qué pasa con el texto de fórmulas desconocidas e inseguras al guardar?

Conservar una fórmula y ejecutarla son ahora dos preguntas separadas, y la política de entrada solo responde la primera. FormulaEntryPolicy en cualquiera de las clases de libro lleva UnknownFunctionMode y UnknownNameMode, ambos con default xlfusmReject, así que asignar una fórmula con una llamada desconocida por la propiedad Formula normal se rechaza antes de que cambien el valor de la celda, la caché de fórmulas o las dependencias. ValidateFormulaEntry reporta la misma decisión sin efectos secundarios. Los caminos de confianza como la carga de archivos, el copiado y la conversión de formato se saltan esa política de entrada de usuario, porque un default estricto jamás debe rechazar símbolos ya presentes en un archivo que usted apenas está abriendo

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // entrada por compatibilidad
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // se almacena, no está autorizada
  Book.SaveAs('rates.xls');
end;

En BIFF8 Classic una llamada desconocida no tiene token propio, así que HotXLS la escribe como Excel escribe las funciones de add-in. La fórmula recibe un token PtgNameX ($59) cuya entrada XTI apunta al SUPBOOK de add-ins con ambos índices de hoja en $FFFE, seguido de los tokens de argumentos y un PtgFuncVar con número de función 255 y un conteo de argumentos que incluye la ranura del nombre. El cuerpo ExternName de respaldo son seis bytes en cero, un byte de longitud y bandera Unicode, el nombre de función en UTF-16, y luego una fórmula de dos bytes $1C $17, un PtgErr que carga #REF!. El escritor rechaza nombres de más de 255 caracteres, más de 29 argumentos y el destino BIFF5. Cómo HotXLS clasifica estas entradas SUPBOOK de add-ins junto a los enlaces a libros externos está explicado en las reglas de clasificación SUPBOOK y XTI de los enlaces externos BIFF. XLSX conserva el texto crudo de la función y ODS conserva su fórmula msoxl:, y en todos los formatos un archivo que guardó =WEBSERVICE(...) reabre con el texto intacto y sigue evaluando a xlfeUnsafeFunctionDenied por defecto

Cómo HotXLS escribe una llamada de fórmula desconocida en BIFF8 clásico: la fórmula lleva un token PtgNameX cuya entrada XTI apunta al SUPBOOK de add-ins con ambos índices de hoja $FFFE, luego los tokens de argumentos y un PtgFuncVar con número de función 255, respaldado por un cuerpo ExternName que termina en un PtgErr de dos bytes $1C $17 que carga #REF!
XLSX conserva el texto crudo de la función y ODS conserva su fórmula msoxl:, así que un archivo que guardó =WEBSERVICE(...) reabre con el texto intacto y sigue evaluando a xlfeUnsafeFunctionDenied por defecto

Si su pipeline evalúa libros que usted no escribió, deje AllowUnsafeFormulaCallbacks en False, mantenga los handlers sobre una allowlist explícita, y conceda opciones por llamada solo donde la fuente de la fórmula es suya. La API completa de callbacks, política de entrada y evaluación está documentada junto al componente de hojas de cálculo HotXLS para Delphi