Artigo Técnico

Callbacks de fórmulas inseguras no HotXLS: CALL e WEBSERVICE

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

Como o HotXLS bloqueia callbacks de fórmulas inseguras: o nome da função é checado contra o conjunto de negação antes de o array de argumentos ser montado ou qualquer resolver rodar, então um =WEBSERVICE(AUDIT_TOKEN()) aninhado é recusado com xlfeUnsafeFunctionDenied e o log de auditoria fica vazio, enquanto chamadas seguras percorrem a cadeia dos bindings LAMBDA e LET até o OnUserFunction
Só quando todos os estágios declinam é que uma função verdadeiramente desconhecida vira #NAME?, e é por isso que a checagem de segurança fica na frente dos estágios de callback em vez de dentro de qualquer handler

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

Como o HotXLS normaliza um nome de função antes da comparação de callback inseguro: espaços em branco são cortados, o nome vai para maiúsculas e um único prefixo _XLFN. ou _XLWS. é removido, então _xlfn.webservice é pego como a grafia nua, e o resultado é casado exatamente contra o conjunto fixo de negação de 20 nomes em lxCalc.pas
A lista cobre os nomes que carregam código nativo, alcançam a rede, conversam com outros processos ou tocam o sistema de arquivos, de DDE, CALL e WEBSERVICE a FWRITE e FILE.DELETE

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

Como o HotXLS grava uma chamada de fórmula desconhecida no BIFF8 clássico: a fórmula carrega um token PtgNameX cuja entrada XTI aponta para o SUPBOOK de add-in com os dois índices de planilha $FFFE, depois os tokens de argumento e um PtgFuncVar com número de função 255, apoiado por um corpo ExternName terminando num PtgErr de dois bytes $1C $17 guardando #REF!
O XLSX mantém o texto cru da função e o ODS mantém sua fórmula msoxl:, então um arquivo que salvou =WEBSERVICE(...) reabre com o texto intacto e ainda avalia para xlfeUnsafeFunctionDenied por padrão

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