Artículo técnico

Callbacks no seguros: frenar CALL y WEBSERVICE en HotXLS

HotXLS se niega a enrutar 20 nombres de fórmulas peligrosos, entre ellos CALL, REGISTER.ID, WEBSERVICE y DDE, hacia tus callbacks de funciones de usuario en Delphi salvo que tú lo actives. La propiedad AllowUnsafeFormulaCallbacks del workbook vale False por defecto, la comprobación corre antes de evaluar cualquier argumento, y una llamada denegada reporta xlfeUnsafeFunctionDenied sin invocar ni un solo handler

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

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

La causa raíz era un único camino de fallback. HotXLS parsea todos los nombres de funciones de Excel que conoce, pero no todo nombre conocido tiene implementación en el motor de cálculo. Los built-in reconocidos pero sin implementar caían antes 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 tu DISCOUNT o tu REGIONRATE. Nombres desconocidos como WEBSERVICE o DDE podían a su vez coincidir con una entrada homónima en el registro del workbook, el registro de proceso o un event handler. La mecánica de ese fallback está cubierta en cómo resuelve HotXLS funciones personalizadas a través de OnUserFunction; el problema era que nada en ese camino preguntaba si el nombre en sí era de esos que un host sensato debería ejecutar jamás

El orden de despacho importa para lo que significa «desconocido» aquí. Una llamada que el motor no puede evaluar de forma nativa se ofrece por turnos a los enlaces léxicos LAMBDA y LET, que resuelve primero el soporte de closures del motor de fórmulas de HotXLS, después a las funciones locales del workbook registradas con RegisterUserFunction, luego a las funciones de proceso de TXLSWorkbook.RegisterGlobalUserFunction, y por último a los eventos OnUserFunction y OnUserFunctionEx. Solo cuando todos declinan se convierte una función verdaderamente desconocida en #NAME?. Cada etapa posterior a la búsqueda de lambdas le entrega el control a código escrito por ti, que es exactamente por lo que la comprobación de seguridad tiene que sentarse delante de toda la cadena y no dentro de ningún handler en concreto

Cómo filtra HotXLS los callbacks de fórmulas no seguras: GetValueItemUserFunction comprueba el nombre antes de que se construya el array de argumentos o corra cualquier resolver, así que un =WEBSERVICE(AUDIT_TOKEN()) anidado sale con lxErrorUnsafeFunctionDenied y el log de auditoría sigue vacío, mientras que las llamadas seguras recorren la cadena desde los enlaces LAMBDA y LET hasta OnUserFunction
Solo cuando cada etapa declina se convierte una función verdaderamente desconocida en #NAME?, que es por lo que la comprobación de seguridad se sienta delante de toda la cadena y no dentro de ningún handler en concreto

¿Qué nombres de funciones bloquea HotXLS por defecto?

XLSFormulaCallbackIsUnsafe en lxCalc.pas guarda un conjunto fijo de denegación de 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, alcanzan la red, hablan con otros procesos o tocan el sistema de archivos. Antes de comparar, la función recorta los espacios de los extremos, pasa el nombre a mayúsculas y quita un único prefijo _XLFN. o _XLWS., así que un _xlfn.webservice escrito por una build de Excel más nueva cae igual que la grafía pelada. La lista vive en la frontera de la calculadora y no en los parsers Classic, XLSX y ODS, lo que mantiene un AST, un stream de tokens BIFF y un workbook convertido comportándose idénticos

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

Dos bordes conviene conocer antes de fiarte de ello. La coincidencia es exacta, así que un handler que registres como MYWEBSERVICE no se ve afectado, y al revés, una UDF interna legítima que se llame justo OPEN o RUN ahora se deniega por defecto. El conjunto de denegación tampoco es una sandbox para tus propios handlers. Si tu rama catch-all ejecuta nombres de plugins arbitrarios, la puerta detiene a los peligrosos famosos y no hace nada más; el fix duradero sigue siendo un handler que compare contra una allowlist explícita con SameText y deje Handled a False para todo lo que no es suyo

¿Por qué la puerta tiene que correr antes de evaluar los argumentos?

Una puerta que salta después de computar los argumentos llega tarde, porque los propios argumentos pueden llamar a tu código. GetValueItemUserFunction comprueba primero el nombre y sale con lxErrorUnsafeFunctionDenied antes de construir el array de argumentos, antes de consultar un resolver o cualquiera de los dos registros, e incluso antes de percatarse de que no hay ningún handler asignado. Ese orden es lo que desactiva el caso anidado de abajo, donde la llamada exterior sería rechazada de todos modos pero una UDF interior de aspecto inofensivo saltaría antes y dejaría su efecto secundario detrás

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 workbook frente a TXLSFormulaEvaluationOptions por llamada

El flag del workbook 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 veredicto para esa llamada y no lo combina con OR junto a la propiedad del workbook. Esa asimetría es deliberada: un trabajo interno de confianza puede autorizar un lookup RTD sin voltear el workbook entero, y un workbook con opt-in global puede aún forzar una evaluación sensible de vuelta a denegar

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

  // el workbook ha hecho opt-in, pero esta evaluación de texto subido no
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // el flag vuelve a False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Conmutar la propiedad del workbook 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 resultado cacheado xlfeUnsafeFunctionDenied podría sobrevivir a un opt-in. El nuevo estado se añadió al final de TXLSFormulaEvaluationStatus después de xlfeFailed, así que tiene ordinal 10 y todos los ordinales existentes conservan su valor; la misma regla de añadir por la cola vale para el campo del record de opciones y para el getter y el setter de IXLSWorkbook, aunque un consumidor construido contra una versión anterior aún necesita recompilar

¿Qué pasa con el texto de fórmulas desconocidas y no seguras al guardar?

Conservar una fórmula y ejecutarla son ahora dos preguntas separadas, y la política de entrada solo responde a la primera. FormulaEntryPolicy en cualquiera de las dos clases de workbook lleva UnknownFunctionMode y UnknownNameMode, ambos con default xlfusmReject, así que asignar una fórmula con una llamada desconocida a través de 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, la copia 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 que ya están presentes en un archivo que estás meramente 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)';  // almacenada, no autorizada
  Book.SaveAs('rates.xls');
end;

En BIFF8 clásico 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 a $FFFE, seguido de los tokens de argumentos y un PtgFuncVar que lleva el 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 a cero, un byte de longitud y flag Unicode, el nombre de función en UTF-16, y luego una fórmula de dos bytes $1C $17, un PtgErr que contiene #REF!. El escritor rechaza nombres de más de 255 caracteres, más de 29 argumentos, y el destino BIFF5. Cómo clasifica HotXLS estas entradas SUPBOOK de add-ins junto a los enlaces externos de workbook lo explica las reglas de clasificación SUPBOOK y XTI para 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(...) se reabre con el texto intacto y sigue evaluando a xlfeUnsafeFunctionDenied por defecto

Cómo escribe HotXLS 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 contiene #REF!
XLSX conserva el texto crudo de la función y ODS conserva su fórmula msoxl:, así que un archivo que guardó =WEBSERVICE(...) se reabre con el texto intacto y sigue evaluando a xlfeUnsafeFunctionDenied por defecto

Si tu pipeline evalúa workbooks que no escribió él, deja AllowUnsafeFormulaCallbacks a False, mantén los handlers sobre una allowlist explícita, y concede opciones por llamada solo donde el origen de la fórmula eres tú. Toda la API de callbacks, política de entrada y evaluación está documentada con el componente de hojas de cálculo HotXLS para Delphi