Motores de planilha avaliam os argumentos antes de chamar a função que os recebe. Uma célula com =WEBSERVICE(AUDIT_TOKEN()) portanto roda AUDIT_TOKEN primeiro, e se a aplicação host responde AUDIT_TOKEN por meio de um handler de funções definidas pelo usuário, esse handler já escreveu a linha de log dele, incrementou o contador ou abriu a conexão quando alguém olha a palavra WEBSERVICE. Uma blocklist dentro do handler é tarde demais. Uma que inspeciona o nome externo depois de os argumentos voltarem também. A recusa precisa ser decidida no nome externo, antes de qualquer argumento ser tocado e antes de qualquer código que o host escreveu ganhar a vez
O cenário onde isso morde é banal. Um serviço aceita arquivos XLS ou XLSX enviados por upload, recalcula no servidor e lê alguns totais de volta. Anos atrás alguém registrou um handler de função do usuário para duas funções de negócio, e no caminho ele ganhou um ramo catch-all que encaminha qualquer nome não reconhecido para uma tabela de plugins. Ninguém do time jamais digitou =WEBSERVICE(...) numa célula. Quem digitou foi quem fez o upload. Manter essa fórmula intacta através de abertura, recálculo e save é um trabalho de fidelidade de arquivo; deixá-la alcançar código host que pode abrir sockets ou gravar arquivos é uma decisão de autorização, e as duas precisam de donos diferentes
Como uma fórmula preservada chega aos seus callbacks Delphi
No motor de fórmulas do HotXLS, qualquer chamada que a calculadora não consegue avaliar nativamente cai numa mesma cadeia de despacho compartilhada, e nomes perigosos chegam lá por duas estradas. Uma estrada são funções built-in que o parser reconhece mas a calculadora não tem implementação: CALL e REGISTER.ID são nomes de função reais do Excel, então analisam limpos e depois caem no mesmo fallback do seu DISCOUNT ou REGIONRATE. A outra estrada são nomes que o parser não conhece de forma alguma, como WEBSERVICE ou DDE, que podem casar com uma entrada de mesmo nome num registro ou ser reivindicados por um event handler catch-all. A mecânica desse fallback está coberta em como o HotXLS resolve funções customizadas por meio do OnUserFunction
A ordem da cadeia decide o que "desconhecido" significa. Um nome vai para a maquinaria de LAMBDA antes de qualquer outra coisa: uma closure ligada por LET, ou um nome definido cuja fórmula é um LAMBDA, ambos descritos em o suporte a closures no motor de fórmulas do HotXLS. Depois vêm as funções locais da pasta de trabalho registradas com RegisterUserFunction, seguidas das funções de processo vindas do método de classe TXLSWorkbook.RegisterGlobalUserFunction, e por último os eventos. O OnUserFunctionEx roda quando está atribuído; o OnUserFunction roda só quando o OnUserFunctionEx não está. Um nome verdadeiramente desconhecido vira #NAME? quando todo estágio declina, enquanto um built-in reconhecido mas não implementado reporta uma função não suportada
Todo estágio depois da busca de LAMBDA entrega o controle a código que você escreveu. É por isso que o HotXLS põe a checagem de segurança na frente de todo estágio que alcança o seu código em vez de dentro de um deles, e por que a checagem fica na calculadora em si em vez de nos três leitores de arquivo. Uma fórmula carregada de tokens BIFF Classic, de texto XLSX ou de uma fórmula msoxl: de ODS acaba como a mesma expressão analisada, então uma checagem cobre os três formatos e uma pasta de trabalho convertida se comporta igual à original
Quais nomes de fórmula o HotXLS nega por padrão?
O HotXLS nega um conjunto fixo de 20 nomes a menos que callbacks sejam explicitamente 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 e FILE.DELETE. No Excel ou na linguagem de macro dele, esses são os nomes que carregam código nativo, alcançam a rede, conversam com outros processos ou tocam o sistema de arquivos. Antes de comparar, o motor corta espaços em branco nas pontas, converte o nome para maiúsculas e remove um único prefixo _XLFN. ou _XLWS.. Um arquivo escrito por uma build mais nova do Excel que armazena _xlfn.webservice é pego exatamente como a grafia nua
O casamento é exato, o que corta para os dois lados. Um handler que você registra como MYWEBSERVICE não é afetado. Uma UDF interna que por acaso se chama OPEN ou RUN é negada por padrão, e o conserto tentador é definir AllowUnsafeFormulaCallbacks como True na pasta de trabalho para que aquele nome volte a funcionar. Renomeie a função em vez disso. Autorizar a pasta de trabalho inteira para resgatar um nome interno também reabilita CALL, DDE e FILE.DELETE para toda fórmula daquele arquivo, incluindo fórmulas que você não escreveu
O conjunto de negação também não cerca os seus próprios handlers. Se um ramo catch-all executa nomes de plugin arbitrários, o gate para os 20 perigosos conhecidos e deixa todo o resto passar para o seu código. A resposta duradoura é parar de escrever handlers catch-all. Registrar cada função pelo nome com RegisterUserFunction transforma o registro em si numa allowlist, e mantém a checagem de entrada de fórmula com significado: quando OnUserFunction ou OnUserFunctionEx está atribuído, o motor não pode saber quais nomes o evento vai reivindicar, então a validação de entrada para de sinalizar nomes de função desconhecidos por completo
procedure TImportService.WireFunctions;
begin
// Um registro por nome: o registro é a allowlist
FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
// Deixe os dois eventos sem atribuir para que nomes desconhecidos continuem
// sendo recusados na entrada e virando #NAME? na avaliação
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 é um variant array zero-based de argumentos já avaliados
if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
begin
Value := Args[0] * (1 - Args[1]);
Handled := True;
end;
end;
Por que o gate precisa rodar antes da avaliação dos argumentos?
O HotXLS checa o nome da função primeiro e recusa a chamada antes de montar o array de argumentos, antes de consultar um resolver ou qualquer um dos registros, e até antes de notar que nenhum handler está atribuído. Essa ordenação é o que derrota a fórmula aninhada da abertura: o WEBSERVICE externo é recusado enquanto AUDIT_TOKEN ainda é um nó filho não avaliado, então o handler do host nunca roda. A última parte da ordenação importa para logs. Uma pasta de trabalho sem handlers e uma com cinco reportam o mesmo status para =WEBSERVICE(...), então um log de rejeição não muda de significado quando alguém liga um handler depois
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'); // efeito colateral em código host
Value := 'token-42';
Handled := True;
end;
end;
procedure TImportService.ProbeNestedCall;
var
Sheet: TXLSWorksheet;
Eval: TXLSFormulaEvaluationResult;
begin
FBook.OnUserFunction := HandleUdf;
Sheet := FBook.Sheets[1]; // índices de planilha começam em 1
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Esperado: Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106 e FAuditLog.Count = 0
end;
O código nativo -106 é a constante pública lxErrorUnsafeFunctionDenied, e o Eval.Issue.Message diz "The formula callback is blocked by the safety policy". O xlfeUnsafeFunctionDenied é o último membro do TXLSFormulaEvaluationStatus, depois de xlfeFailed. Código que mapeia status com um case e um ramo else para falha genérica vai arquivar uma negação como "avaliação falhou" a menos que nomeie o status explicitamente, e para um pipeline de upload esse é o desfecho que vale reportar de volta a quem enviou o arquivo
Padrão da pasta de trabalho versus TXLSFormulaEvaluationOptions por chamada
A flag da pasta de trabalho define o padrão e a opção por chamada tem a palavra final. TXLSWorkbook.AllowUnsafeFormulaCallbacks e TXLSXWorkbook.AllowUnsafeFormulaCallbacks começam em False e governam recálculo comum, Calculate, o EvaluateFormulaAt de três argumentos, EvaluateAt num template de avaliação, fórmulas de lista de validação de dados, views somente leitura e, no XLSX, todo worker do pool de recálculo paralelo. Qualquer ponto de entrada que recebe um record TXLSFormulaEvaluationOptions explícito usa Options.AllowUnsafeFormulaCallbacks como o veredito daquela chamada e não faz OR com a propriedade da pasta de trabalho
Essa assimetria é deliberada, e é o padrão certo. Um job interno confiável pode autorizar um lookup RTD sem virar a pasta de trabalho inteira, e uma pasta de trabalho globalmente autorizada ainda pode forçar uma avaliação sensível de volta para negado. O XLSDefaultFormulaEvaluationOptions devolve a flag como False, então montar o record a partir dessa função significa negado a menos que você a defina de propósito. Um caminho ignora a flag por completo: quando regras de formatação condicional Classic são avaliadas para renderização, callbacks são negados independente do que a pasta de trabalho diga
var
Sheet: TXLSWorksheet;
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
Sheet := FBook.Sheets[1];
// a pasta de trabalho segue travada, uma chamada confiável passa
FBook.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// a pasta de trabalho autorizou, mas esta avaliação de texto enviado não
FBook.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // a flag volta para False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message); // LogRejected é uma rotina sua
end;
Alternar a propriedade da pasta de trabalho também marca toda fórmula do grafo de dependências como suja, nas duas classes de pasta de trabalho. Sem isso, um valor em cache calculado enquanto os callbacks eram permitidos poderia ser servido depois de revogados, ou uma negação em cache poderia sobreviver a uma autorização. Definir a propriedade com o valor que ela já tem é um no-op e deixa o cache em paz, então alterná-la dentro de um loop só custa um recálculo completo quando o valor de fato muda
O que acontece com texto de fórmulas desconhecidas e inseguras no save?
Manter uma fórmula e executá-la são duas perguntas separadas, e a política de entrada só responde a primeira. A FormulaEntryPolicy em qualquer classe de pasta de trabalho carrega UnknownFunctionMode e UnknownNameMode, ambos com xlfusmReject como padrão. Atribuir uma fórmula com uma chamada desconhecida pela propriedade Formula normal levanta EArgumentException com "Formula entry rejected: unknown function" antes de o valor da célula, o cache de fórmulas ou as dependências mudarem. O ValidateFormulaEntry devolve a mesma decisão como um record, sem efeitos colaterais. Caminhos confiáveis como carga de arquivo, cópia e conversão de formato não passam pela política de entrada do usuário, porque um padrão estrito nunca deve rejeitar símbolos já presentes num arquivo que você está apenas abrindo
var
Sheet: TXLSWorksheet;
Policy: TXLSFormulaEntryPolicy;
Check: TXLSFormulaEntryValidationResult;
begin
Sheet := FBook.Sheets[1];
Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
// política padrão, sem handler para ACME_RATE:
// Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject
Policy := FBook.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // entrada por compatibilidade
FBook.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // armazenada, não autorizada
FBook.SaveAs('rates.xls');
end;
O Classic BIFF8 não tem token para um nome de função arbitrário, então o HotXLS grava uma chamada desconhecida do jeito que o Excel grava funções de add-in. A fórmula recebe um token PtgNameX ($59) cuja entrada XTI aponta para o SUPBOOK de add-in com os dois índices de planilha em $FFFE, seguido dos tokens de argumento e um PtgFuncVar carregando número de função 255 e uma contagem de argumentos que inclui o slot de nome. O corpo ExternName de apoio são seis bytes zero, um byte de comprimento e a flag Unicode, o nome da função em UTF-16, e então uma fórmula de dois bytes $1C $17, um PtgErr guardando #REF!. O writer aceita nomes de 1 a 255 caracteres com no máximo 29 argumentos, e mira apenas no BIFF8; um save BIFF5 não consegue carregar a chamada. Como essas entradas de SUPBOOK de add-in ficam ao lado de links externos de pasta de trabalho está explicado em as regras de classificação de SUPBOOK e XTI para links externos do BIFF
O XLSX mantém o texto cru da função e o ODS mantém sua fórmula msoxl:. Nos três formatos, um arquivo que salvou =WEBSERVICE(...) reabre com o nome intacto, e a avaliação padrão dele continua retornando xlfeUnsafeFunctionDenied sem chamar o host. Salvar nunca promove as permissões de uma fórmula: o mesmo texto que era negado antes do round trip continua negado depois
Para um servidor que recalcula pastas de trabalho que ele não escreveu, a configuração que se sustenta é curta: deixe AllowUnsafeFormulaCallbacks em False, registre funções pelo nome em vez de atribuir um evento catch-all, e passe opções por chamada só onde o texto da fórmula vem do seu próprio código. A referência de callbacks, política de entrada e avaliação vem com a documentação do HotXLS Delphi spreadsheet component