Artigo Técnico

Callbacks inseguros no HotXLS: barrar CALL e WEBSERVICE

O HotXLS recusa encaminhar 20 nomes de fórmulas perigosos, incluindo CALL, REGISTER.ID, WEBSERVICE e DDE, para os seus callbacks Delphi de funções do utilizador, a menos que ative a opção. A propriedade do livro AllowUnsafeFormulaCallbacks assume por predefinição False, a verificação corre antes de qualquer argumento ser avaliado, e uma chamada recusada reporta xlfeUnsafeFunctionDenied sem invocar um único handler

O cenário que tornou isto necessário é banal. Um serviço aceita ficheiros XLS ou XLSX carregados, recalcula-os do lado do servidor e lê de volta meia dúzia de totais. A aplicação anfitriã registou há anos um handler OnUserFunction para um par de funções de negócio, e em algum momento desse caminho o handler ganhou um ramo apanha-tudo que encaminha tudo o que não reconhece para uma tabela de plugins. Ninguém na equipa escreveu =WEBSERVICE(...) numa célula. Quem o fez foi quem carregou o ficheiro. Manter essa fórmula intacta através de abertura, recálculo e gravação é uma funcionalidade de fidelidade ao ficheiro. Deixá-la chegar a código anfitrião que pode abrir sockets ou ficheiros é uma decisão de autorização, e enquanto o HotXLS não separou as duas coisas, era a biblioteca a tomar essa decisão em seu nome, em silêncio

Porque é que preservar uma fórmula se transformou em permissão para a executar?

A causa raiz era um único caminho de recurso. O HotXLS analisa todos os nomes de funções do Excel que conhece, mas nem todo nome conhecido tem uma implementação no motor de cálculo. Os built-ins reconhecidos mas não implementados caíam no mesmo recurso de função definida pelo utilizador que os nomes genuinamente personalizados, pelo que o CALL e o REGISTER.ID partilhavam um caminho de despacho com o seu DISCOUNT ou REGIONRATE. Nomes desconhecidos como WEBSERVICE ou DDE podiam da mesma forma corresponder a uma entrada com o mesmo nome no registo do livro, no registo do processo ou num event handler. A mecânica desse recurso está coberta em como o HotXLS resolve funções personalizadas através do OnUserFunction; o problema era que nada nesse caminho perguntava se o próprio nome era um que um anfitrião são deveria executar

A ordem de despacho importa para o que «desconhecido» significa aqui. Uma chamada que o motor não consegue avaliar nativamente é oferecida por sua vez a ligações lexicais LAMBDA e LET, que o suporte de closures no motor de fórmulas do HotXLS resolve primeiro, depois a funções locais do livro registadas com RegisterUserFunction, depois a funções de todo o processo de TXLSWorkbook.RegisterGlobalUserFunction, e finalmente aos eventos OnUserFunction e OnUserFunctionEx. Só quando todos declinam é que uma função verdadeiramente desconhecida se torna #NAME?. Cada estágio depois da procura de lambda entrega o controlo a código que escreveu, o que é exatamente por isso que a verificação de segurança tem de ficar à frente de toda a cadeia em vez de dentro de qualquer handler

Como o HotXLS barra callbacks de fórmulas inseguras: o GetValueItemUserFunction verifica o nome antes de a matriz de argumentos ser construída ou qualquer resolver correr, pelo que um =WEBSERVICE(AUDIT_TOKEN()) aninhado sai com lxErrorUnsafeFunctionDenied e o registo de auditoria fica vazio, enquanto chamadas seguras percorrem a cadeia das ligações LAMBDA e LET até ao OnUserFunction
Só quando todos os estágios declinam é que uma função verdadeiramente desconhecida se torna #NAME?, razão pela qual a verificação de segurança fica à frente de toda a cadeia em vez de dentro de qualquer handler

Que nomes de funções bloqueia o HotXLS por predefinição?

O XLSFormulaCallbackIsUnsafe em lxCalc.pas guarda um conjunto fixo de recusa com 20 nomes: 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. São os nomes que, no Excel ou na sua linguagem de macros, carregam código nativo, alcançam a rede, falam com outros processos ou tocam no sistema de ficheiros. Antes da comparação, a função corta o espaço em branco em redor, põe o nome em maiúsculas e remove um único prefixo _XLFN. ou _XLWS., pelo que um _xlfn.webservice escrito por uma build mais recente do Excel é apanhado tal como a grafia simples. A lista vive na fronteira da calculadora em vez de nos parsers Classic, XLSX e ODS, o que mantém um único AST, um único stream de tokens BIFF e um único livro convertido a comportar-se de forma idêntica

Como o HotXLS normaliza um nome de função antes da comparação de callback inseguro: o espaço em branco é cortado, o nome é posto em maiúsculas e um único prefixo _XLFN. ou _XLWS. é removido, pelo que _xlfn.webservice é apanhado como a grafia simples, e depois o resultado é comparado exatamente com o conjunto fixo de recusa de 20 nomes em lxCalc.pas
A lista cobre os nomes que carregam código nativo, alcançam a rede, falam com outros processos ou tocam no sistema de ficheiros, de DDE, CALL e WEBSERVICE a FWRITE e FILE.DELETE

Duas arestas valem a pena conhecer antes de confiar nisso. A correspondência é exata, pelo que um handler que registe como MYWEBSERVICE não é afetado, e inversamente uma UDF interna legítima que por acaso se chame OPEN ou RUN passa a ser recusada por predefinição. O conjunto de recusa também não é uma sandbox para os seus próprios handlers. Se o seu ramo apanha-tudo executa nomes de plugins arbitrários, o portão para os perigosos famosos e mais nada; a correção duradoura continua a ser um handler que compare com uma allowlist explícita através do SameText e deixe Handled a False para tudo o que não lhe pertence

Porque é que o portão tem de correr antes da avaliação dos argumentos?

Um portão que dispara depois de os argumentos serem calculados é tarde demais, porque os próprios argumentos podem chamar o seu código. O GetValueItemUserFunction verifica o nome primeiro e sai com lxErrorUnsafeFunctionDenied antes de construir a matriz de argumentos, antes de consultar um resolver ou qualquer dos registos, e mesmo antes de reparar que não há handler atribuído nenhum. Essa ordenação é o que derrota o caso aninhado abaixo, em que a chamada exterior seria recusada de qualquer forma, mas uma UDF interior de aparência inofensiva dispararia primeiro e deixaria o seu efeito colateral para trá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');   // efeito colateral em código anfitrião
    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, e o FAuditLog continua vazio

Predefinição do livro contra TXLSFormulaEvaluationOptions por chamada

A flag do livro é a predefinição e a opção por chamada tem a palavra final. O TXLSWorkbook.AllowUnsafeFormulaCallbacks e o TXLSXWorkbook.AllowUnsafeFormulaCallbacks governam o recálculo ordinário, o Calculate, o EvaluateFormulaAt de dois argumentos, os templates de avaliação, as vistas só de leitura e, em XLSX, todos os workers da pool de recálculo paralelo. Qualquer ponto de entrada que aceite um registo TXLSFormulaEvaluationOptions explícito toma o Options.AllowUnsafeFormulaCallbacks como veredicto para essa chamada e não o combina com OR com a propriedade do livro. Essa assimetria é deliberada: um job interno de confiança pode autorizar uma consulta RTD sem virar o livro inteiro, e um livro globalmente aderido pode ainda forçar uma avaliação sensível de volta à recusa

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // o livro continua fechado, uma chamada de confiança passa
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // o livro aderiu, mas esta avaliação de texto carregado não
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // a flag volta a False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Comutar a propriedade do livro também marca o grafo de dependências como dirty em ambos os motores. Sem esse passo, um resultado em cache calculado enquanto os callbacks eram permitidos podia ser servido depois de terem sido revogados, ou um resultado em cache xlfeUnsafeFunctionDenied podia sobreviver a uma adesão. O novo estado foi acrescentado a TXLSFormulaEvaluationStatus depois de xlfeFailed, pelo que tem ordinal 10 e cada ordinal existente conserva o seu valor; a mesma regra de acrescentar no fim aplica-se ao campo do registo de opções e ao getter e setter do IXLSWorkbook, embora um consumidor construído contra uma versão mais antiga precise ainda de recompilar

O que acontece ao texto de fórmulas desconhecidas e inseguras ao gravar?

Manter uma fórmula e executá-la são agora duas perguntas separadas, e a política de entrada só responde à primeira. O FormulaEntryPolicy em qualquer das classes de livro transporta UnknownFunctionMode e UnknownNameMode, ambos por predefinição xlfusmReject, pelo que atribuir uma fórmula com uma chamada desconhecida através da propriedade normal Formula é recusado antes de o valor da célula, a cache de fórmulas ou as dependências mudarem. O ValidateFormulaEntry reporta a mesma decisão sem efeitos colaterais. Os caminhos de confiança como o carregamento de ficheiros, a cópia e a conversão de formato contornam essa política de entrada do utilizador, porque uma predefinição estrita nunca deve recusar símbolos já presentes num ficheiro que está apenas a abrir

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // entrada por compatibilidade
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // guardada, não autorizada
  Book.SaveAs('rates.xls');
end;

No BIFF8 clássico uma chamada desconhecida não tem token próprio, pelo que o HotXLS escreve-a da forma como o Excel escreve funções de add-ins. A fórmula recebe um token PtgNameX ($59) cuja entrada XTI aponta para o SUPBOOK de add-ins com ambos os índices de folha postos a $FFFE, seguido dos tokens de argumentos e de um PtgFuncVar com o número de função 255 e uma contagem de argumentos que inclui a ranhura do nome. O corpo ExternName de suporte são seis bytes a zero, um byte de comprimento e flag Unicode, o nome da função em UTF-16, e depois uma fórmula de dois bytes $1C $17, um PtgErr com #REF!. O escritor recusa nomes com mais de 255 caracteres, mais de 29 argumentos, e o destino BIFF5. Como o HotXLS classifica estas entradas SUPBOOK de add-ins ao lado das ligações a livros externos está explicado em as regras de classificação SUPBOOK e XTI para ligações externas BIFF. O XLSX mantém o texto bruto da função e o ODS mantém a sua fórmula msoxl:, e em todos os formatos um ficheiro que gravou =WEBSERVICE(...) reabre com o texto intacto e continua a avaliar para xlfeUnsafeFunctionDenied por predefinição

Como o HotXLS escreve uma chamada de fórmula desconhecida no BIFF8 clássico: a fórmula transporta um token PtgNameX cuja entrada XTI aponta para o SUPBOOK de add-ins com ambos os índices de folha $FFFE, depois os tokens de argumentos e um PtgFuncVar com o número de função 255, suportado por um corpo ExternName que termina numa fórmula de dois bytes $1C $17, um PtgErr com #REF!
O XLSX mantém o texto bruto da função e o ODS mantém a sua fórmula msoxl:, pelo que um ficheiro que gravou =WEBSERVICE(...) reabre com o texto intacto e continua a avaliar para xlfeUnsafeFunctionDenied por predefinição

Se o seu pipeline avalia livros que não escreveu, deixe o AllowUnsafeFormulaCallbacks em False, mantenha os handlers numa allowlist explícita, e conceda opções por chamada apenas onde a origem da fórmula é sua. A API completa de callbacks, política de entrada e avaliação está documentada com o componente de folhas de cálculo HotXLS para Delphi