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