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
¿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
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
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