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