Artículo técnico

Callbacks no seguros: frenar CALL y WEBSERVICE en HotXLS

Los motores de hojas de cálculo evalúan los argumentos antes de llamar a la función que los recibe. Una celda con =WEBSERVICE(AUDIT_TOKEN()) ejecuta por tanto AUDIT_TOKEN primero, y si la aplicación host responde AUDIT_TOKEN a través de un handler de funciones definidas por el usuario, ese handler ya ha escrito su línea de log, incrementado su contador o abierto su conexión para cuando alguien mira la palabra WEBSERVICE. Una blocklist dentro del handler llega tarde. Y una que inspeccione el nombre exterior después de que los argumentos vuelvan, también. El rechazo tiene que decidirse sobre el nombre exterior, antes de tocar cualquier argumento y antes de que el código que escribió el host tenga su turno

El escenario donde esto muerde es mundano. Un servicio acepta archivos XLS o XLSX subidos, los recalcula en el servidor y lee unos totales de vuelta. Hace años alguien registró un handler de funciones de usuario para dos funciones de negocio, y por el camino le creció una rama catch-all que reenvía cualquier nombre no reconocido a una tabla de plugins. Nadie del equipo tecleó jamás =WEBSERVICE(...) en una celda. Lo hizo el uploader. Conservar esa fórmula intacta a través de apertura, recálculo y guardado es un trabajo de fidelidad de archivo; dejar que llegue a código del host que puede abrir sockets o escribir archivos es una decisión de autorización, y las dos cosas necesitan dueños distintos

Cómo llega una fórmula preservada a sus callbacks de Delphi

En el motor de fórmulas de HotXLS, cualquier llamada que la calculadora no puede evaluar de forma nativa cae en una misma cadena de dispatch compartida, y los nombres peligrosos llegan allí por dos caminos. Uno son funciones integradas que el parser reconoce pero la calculadora no tiene implementadas: CALL y REGISTER.ID son nombres reales de funciones de Excel, así que parsean limpio y luego aterrizan en el mismo fallback que su DISCOUNT o REGIONRATE. El otro son nombres que el parser no conoce en absoluto, como WEBSERVICE o DDE, que pueden coincidir con una entrada homónima de un registro o ser reclamados por un handler de evento catch-all. La mecánica de ese fallback está cubierta en cómo HotXLS resuelve funciones personalizadas mediante OnUserFunction

El orden de la cadena decide lo que significa «desconocido». Un nombre va a la maquinaria LAMBDA antes que a nada: una closure ligada por LET, o un nombre definido cuya fórmula es una LAMBDA, ambos descritos en el soporte de closures del motor de fórmulas de HotXLS. Después vienen las funciones locales del workbook registradas con RegisterUserFunction, siguen las de proceso completo del método de clase TXLSWorkbook.RegisterGlobalUserFunction y al final los eventos. OnUserFunctionEx corre si está asignado; OnUserFunction corre solo cuando OnUserFunctionEx no lo está. Un nombre verdaderamente desconocido se convierte en #NAME? cuando todas las etapas han declinado, mientras que un integrado reconocido pero sin implementar reporta una función no soportada

Todas las etapas posteriores a la búsqueda LAMBDA le entregan el control a código suyo. Por eso HotXLS pone su comprobación de seguridad delante de cada etapa que alcanza su código en lugar de dentro de una sola, y por eso la comprobación vive en la propia calculadora en vez de en los tres lectores de archivo. Una fórmula cargada desde tokens BIFF clásicos, desde texto XLSX o desde una fórmula msoxl: de ODS acaba como la misma expresión parseada, así que una única comprobación cubre los tres formatos y un workbook convertido se comporta igual que el original

Cómo HotXLS frena los callbacks de fórmulas no seguras: el nombre de la función se comprueba contra el conjunto de denegación antes de construir el array de argumentos o de que corra cualquier resolver, así que un =WEBSERVICE(AUDIT_TOKEN()) anidado se rechaza con xlfeUnsafeFunctionDenied y el log de auditoría queda vacío, mientras que las llamadas seguras recorren la cadena desde las bindings de LAMBDA y LET hasta OnUserFunction
Solo cuando todas las etapas declinan se convierte una función verdaderamente desconocida en #NAME?, y por eso la comprobación de seguridad se sienta delante de las etapas de callback en lugar de dentro de un handler cualquiera

¿Qué nombres de fórmula deniega HotXLS por defecto?

HotXLS deniega un conjunto fijo de 20 nombres salvo que los callbacks estén explícitamente permitidos: 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. En Excel o en su lenguaje de macros estos son los nombres que cargan código nativo, alcanzan la red, hablan con otros procesos o tocan el sistema de archivos. Antes de comparar, el motor recorta los espacios en blanco alrededor, pasa el nombre a mayúsculas y quita un único prefijo _XLFN. o _XLWS.. Un archivo escrito por una build más nueva de Excel que guarde _xlfn.webservice se caza exactamente igual que la escritura desnuda

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. para que _xlfn.webservice se cace igual que la escritura desnuda, y luego el resultado se compara exactamente contra el conjunto fijo de denegación de 20 nombres
La lista cubre 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

La coincidencia es exacta, lo que corta en ambos sentidos. Un handler que usted registra como MYWEBSERVICE no se ve afectado. Una UDF interna que se llame justamente OPEN o RUN se deniega por defecto, y la reparación tentadora es poner AllowUnsafeFormulaCallbacks a True en el workbook para que ese nombre vuelva a funcionar. Renombre la función mejor. Optar por todo el workbook para rescatar un nombre interno también reactiva CALL, DDE y FILE.DELETE para cada fórmula de ese archivo, incluidas las que usted no escribió

El conjunto de denegación tampoco hace nada por cercar sus propios handlers. Si una rama catch-all ejecuta nombres de plugin arbitrarios, la puerta detiene los 20 nombres peligrosos conocidos y deja pasar todo lo demás hasta su código. La respuesta duradera es dejar de escribir handlers catch-all. Registrar cada función por nombre con RegisterUserFunction convierte el propio registro en una allowlist, y mantiene con sentido la comprobación de entrada de fórmulas: cuando OnUserFunction o OnUserFunctionEx está asignado, el motor no puede saber qué nombres reclamará el evento, así que la validación de entrada deja de marcar los nombres de función desconocidos por completo

procedure TImportService.WireFunctions;
begin
  // Un registro por nombre: el registro es la allowlist
  FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
  FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
  // Deje ambos eventos sin asignar para que los nombres desconocidos sigan
  // rechazándose en la entrada y acabando como #NAME? en la evaluación
  FBook.OnUserFunction := nil;
  FBook.OnUserFunctionEx := nil;
end;

procedure TImportService.HandleDiscount(Sender: TObject;
  const FunctionName: WideString; const Args: Variant;
  var Value: Variant; var Handled: Boolean);
begin
  // Args es un variant array zero-based de argumentos ya evaluados
  if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
  begin
    Value := Args[0] * (1 - Args[1]);
    Handled := True;
  end;
end;

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

HotXLS comprueba primero el nombre de la función y rechaza la llamada antes de construir el array de argumentos, antes de consultar un resolver o cualquiera de los dos registros, e incluso antes de notar que no hay ningún handler asignado. Ese orden es lo que derrota a la fórmula anidada de la apertura: el WEBSERVICE exterior se rechaza mientras AUDIT_TOKEN sigue siendo un nodo hijo sin evaluar, así que el handler del host nunca corre. La última parte del orden importa para el logging. Un workbook sin handlers y uno con cinco reportan el mismo estado para =WEBSERVICE(...), así que un log de rechazos no cambia de significado cuando alguien conecta luego un handler

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 lateral en código del host
    Value := 'token-42';
    Handled := True;
  end;
end;

procedure TImportService.ProbeNestedCall;
var
  Sheet: TXLSWorksheet;
  Eval: TXLSFormulaEvaluationResult;
begin
  FBook.OnUserFunction := HandleUdf;
  Sheet := FBook.Sheets[1];                  // los índices de worksheet empiezan en 1
  Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
  // Esperado: Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
  // Eval.Issue.NativeCode = -106 y FAuditLog.Count = 0
end;

El código nativo -106 es la constante pública lxErrorUnsafeFunctionDenied, y Eval.Issue.Message dice «The formula callback is blocked by the safety policy». xlfeUnsafeFunctionDenied es el último miembro de TXLSFormulaEvaluationStatus, tras xlfeFailed. El código que mapea estados con un case y una rama else para fallo genérico archivará una denegación bajo «fallo de evaluación» salvo que nombre el estado explícitamente, y para un pipeline de subidas ese es justamente el desenlace que vale la pena reportar al que subió el archivo

Default del workbook frente a TXLSFormulaEvaluationOptions por llamada

El flag del workbook fija el default y la opción por llamada tiene la última palabra. TXLSWorkbook.AllowUnsafeFormulaCallbacks y TXLSXWorkbook.AllowUnsafeFormulaCallbacks arrancan en False y gobiernan el recálculo ordinario, Calculate, el EvaluateFormulaAt de tres argumentos, EvaluateAt sobre una plantilla de evaluación, las fórmulas de listas de validación de datos, las vistas de solo lectura y, en XLSX, cada worker del pool de recálculo paralelo. Cualquier punto de entrada que reciba un registro explícito TXLSFormulaEvaluationOptions usa Options.AllowUnsafeFormulaCallbacks como veredicto para esa llamada y no lo mezcla con la propiedad del workbook

Esa asimetría es deliberada, y es el default correcto. Un trabajo interno de confianza puede autorizar una consulta RTD sin voltear todo el workbook, y un workbook con opt-in global puede aun así forzar una evaluación delicada de vuelta a denegar. XLSDefaultFormulaEvaluationOptions devuelve el flag como False, así que construir el registro desde esa función significa denegación salvo que usted lo active a propósito. Un camino ignora el flag por completo: cuando las reglas de conditional formatting clásicas se evalúan para renderizado, los callbacks se deniegan diga lo que diga el workbook

var
  Sheet: TXLSWorksheet;
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  Sheet := FBook.Sheets[1];

  // el workbook sigue bloqueado, se deja pasar una llamada de confianza
  FBook.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // workbook con opt-in, pero esta evaluación de texto subido no
  FBook.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);   // LogRejected es rutina suya
end;

Cambiar la propiedad del workbook también marca todas las fórmulas del grafo de dependencias como dirty, en ambas clases de workbook. Sin eso, un valor cacheado mientras los callbacks estaban permitidos podría servirse después de revocarlos, o una denegación cacheada podría sobrevivir a un opt-in. Asignar a la propiedad el valor que ya tiene es un no-op y deja la caché en paz, así que voltearla dentro de un bucle solo cuesta un recálculo completo cuando el valor cambia de verdad

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

Conservar una fórmula y ejecutarla son dos preguntas distintas, y la política de entrada solo responde la primera. FormulaEntryPolicy en cualquiera de las dos clases de workbook lleva UnknownFunctionMode y UnknownNameMode, ambos con default xlfusmReject. Asignar una fórmula con una llamada desconocida a través de la propiedad Formula normal lanza EArgumentException con «Formula entry rejected: unknown function» antes de que cambien el valor de la celda, la caché de fórmulas o las dependencias. ValidateFormulaEntry devuelve la misma decisión como registro, sin efectos laterales. Los caminos de confianza, como cargar un archivo, copiar y la conversión de formato, no pasan por la 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 usted solo está abriendo

var
  Sheet: TXLSWorksheet;
  Policy: TXLSFormulaEntryPolicy;
  Check: TXLSFormulaEntryValidationResult;
begin
  Sheet := FBook.Sheets[1];
  Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
  // política por defecto, sin handler para ACME_RATE:
  // Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject

  Policy := FBook.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // entrada por compatibilidad
  FBook.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // almacenada, no autorizada
  FBook.SaveAs('rates.xls');
end;

El BIFF8 clásico no tiene token para un nombre de función arbitrario, así que HotXLS escribe una llamada desconocida tal y 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 puestos a $FFFE, seguido de los tokens de argumento y un PtgFuncVar que lleva el número de función 255 y un recuento de argumentos que incluye el hueco del nombre. El cuerpo del ExternName que lo respalda son seis bytes a cero, un byte de longitud y el flag Unicode, el nombre de función en UTF-16, y luego una fórmula de dos bytes $1C $17, un PtgErr que lleva #REF!. El escritor acepta nombres de 1 a 255 caracteres con un máximo de 29 argumentos, y apunta solo a BIFF8; un guardado BIFF5 no puede transportar la llamada. Cómo se sientan estas entradas SUPBOOK de add-in junto a los enlaces a workbooks externos lo explican las reglas de clasificación SUPBOOK y XTI para enlaces externos BIFF

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

XLSX conserva el texto crudo de la función y ODS conserva su fórmula msoxl:. En los tres formatos, un archivo que guardó =WEBSERVICE(...) reabre con el nombre intacto, y su evaluación por defecto sigue devolviendo xlfeUnsafeFunctionDenied sin llamar al host. Guardar jamás mejora los permisos de una fórmula: el mismo texto que se denegó antes del round trip se deniega después

Para un servidor que recalcula workbooks que no escribió él, la configuración que aguanta es corta: dejar AllowUnsafeFormulaCallbacks en False, registrar las funciones por nombre en lugar de asignar un evento catch-all, y pasar opciones por llamada solo donde el texto de la fórmula viene de código suyo. La referencia de callbacks, política de entrada y evaluación se envía con la documentación del componente de hojas de cálculo HotXLS para Delphi