I motori di foglio elettronico valutano gli argomenti prima di chiamare la funzione che li riceve. Una cella che contiene =WEBSERVICE(AUDIT_TOKEN()) esegue quindi AUDIT_TOKEN per primo, e se l'applicazione host risponde a AUDIT_TOKEN tramite un handler di funzioni definite dall'utente, quel handler ha già scritto la sua riga di log, incrementato il suo contatore o aperto la sua connessione quando qualcuno guarda la parola WEBSERVICE. Una blocklist dentro il handler arriva troppo tardi. Arriva troppo tardi anche una che ispeziona il nome esterno dopo che gli argomenti sono tornati. Il rifiuto va deciso sul nome esterno, prima che qualsiasi argomento venga toccato e prima che qualsiasi codice scritto dall'host riceva il suo turno
Il contesto dove questo morde è banale. Un servizio accetta file XLS o XLSX caricati, li ricalcola sul server e rilegge qualche totale. Anni fa qualcuno ha registrato un handler di funzioni utente per due funzioni di business, e nel frattempo gli è cresciuto un ramo catch-all che inoltra qualsiasi nome non riconosciuto a una tabella di plugin. Nessuno nel team ha mai digitato =WEBSERVICE(...) in una cella. L'uploader sì. Tenere quella formula intatta attraverso open, recalc e save è un compito di file fidelity; lasciarla arrivare a codice host capace di aprire socket o scrivere file è una decisione di autorizzazione, e le due cose servono responsabili diversi
Come una formula preservata arriva ai vostri callback Delphi
Nel motore di formule di HotXLS, qualsiasi chiamata che il calcolatore non può valutare in modo nativo cade in un'unica catena di dispatch condivisa, e i nomi pericolosi arrivano lì per due strade. Una è quella delle funzioni built-in che il parser riconosce ma per le quali il calcolatore non ha implementazione: CALL e REGISTER.ID sono nomi di funzioni Excel reali, quindi si parsano puliti e poi atterrano nella stessa fallback del vostro DISCOUNT o REGIONRATE. L'altra è quella dei nomi che il parser non conosce affatto, come WEBSERVICE o DDE, che possono combaciare con una voce omonima in un registro o essere rivendicati da un handler di evento catch-all. La meccanica di quella fallback è coperta in come HotXLS risolve le funzioni personalizzate tramite OnUserFunction
L'ordine della catena decide che cosa significa «sconosciuto». Un nome va prima di tutto alla macchina LAMBDA: una closure legata da LET, o un nome definito la cui formula è una LAMBDA, entrambi descritti in il supporto delle closure nel motore di formule di HotXLS. Vengono poi le funzioni locali del workbook registrate con RegisterUserFunction, seguite dalle funzioni a livello di processo dal metodo di classe TXLSWorkbook.RegisterGlobalUserFunction, e per ultime gli eventi. OnUserFunctionEx gira quando è assegnato; OnUserFunction gira solo quando OnUserFunctionEx non lo è. Un nome davvero sconosciuto diventa #NAME? quando ogni stage ha declinato, mentre un built-in riconosciuto ma non implementato riporta una funzione non supportata
Ogni stage dopo la ricerca LAMBDA passa il controllo a codice scritto da voi. Ecco perché HotXLS mette il suo controllo di sicurezza davanti a ogni stage che raggiunge il vostro codice anziché dentro uno qualsiasi di essi, e perché il controllo sta nel calcolatore stesso anziché nei tre lettori di file. Una formula caricata da token Classic BIFF, da testo XLSX o da una formula ODS msoxl: finisce come la stessa espressione parsata, quindi un solo controllo copre tutti e tre i formati e un workbook convertito si comporta come l'originale
Quali nomi di formule nega HotXLS per default?
HotXLS nega un insieme fisso di 20 nomi a meno che i callback non siano esplicitamente consentiti: 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. In Excel o nel suo linguaggio macro questi sono i nomi che caricano codice nativo, raggiungono la rete, parlano ad altri processi o toccano il file system. Prima del confronto, il motore taglia gli spazi ai bordi, mette il nome in maiuscolo e rimuove un singolo prefisso _XLFN. o _XLWS.. Un file scritto da una build di Excel più recente che conserva _xlfn.webservice viene colto esattamente come la grafia nuda
Il confronto è esatto, il che taglia in entrambe le direzioni. Un handler che registrate come MYWEBSERVICE non è toccato. Una UDF interna che per caso si chiama OPEN o RUN viene negata per default, e la riparazione allettante è impostare AllowUnsafeFormulaCallbacks a True sul workbook così quel nome torna a funzionare. Rinominate invece la funzione. Consentire l'opt-in dell'intero workbook per salvare un nome interno riattiva anche CALL, DDE e FILE.DELETE per ogni formula in quel file, comprese le formule che non avete scritto voi
Il deny set inoltre non recinta in alcun modo i vostri handler. Se un ramo catch-all esegue nomi di plugin arbitrari, il filtro ferma i 20 nomi pericolosi noti e lascia passare tutto il resto al vostro codice. La risposta duratura è smettere di scrivere handler catch-all. Registrare ogni funzione per nome con RegisterUserFunction trasforma il registro stesso in una allowlist, e tiene significativo il controllo di entrata delle formule: quando OnUserFunction o OnUserFunctionEx è assegnato, il motore non può sapere quali nomi l'evento rivendicherà, quindi la validazione di entrata smette del tutto di segnalare i nomi di funzione sconosciuti
procedure TImportService.WireFunctions;
begin
// Una registrazione per nome: il registro è la allowlist
FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
// Lascia entrambi gli eventi non assegnati così i nomi sconosciuti
// restano rifiutati all'entrata e finiscono come #NAME? alla valutazione
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 è un variant array a base zero di argomenti già valutati
if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
begin
Value := Args[0] * (1 - Args[1]);
Handled := True;
end;
end;
Perché il filtro deve girare prima della valutazione degli argomenti?
HotXLS controlla prima il nome della funzione e rifiuta la chiamata prima di costruire l'array di argomenti, prima di consultare un resolver o uno dei due registri, e perfino prima di notare che nessun handler è assegnato. Quell'ordine è ciò che sconfigge la formula annidata dell'apertura: il WEBSERVICE esterno viene rifiutato mentre AUDIT_TOKEN è ancora un nodo figlio non valutato, quindi il handler host non gira mai. L'ultima parte dell'ordine conta per il logging. Un workbook senza handler e un workbook con cinque handler riportano lo stesso stato per =WEBSERVICE(...), così un log di rifiuti non cambia significato quando qualcuno più avanti collega un handler
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'); // effetto collaterale in codice host
Value := 'token-42';
Handled := True;
end;
end;
procedure TImportService.ProbeNestedCall;
var
Sheet: TXLSWorksheet;
Eval: TXLSFormulaEvaluationResult;
begin
FBook.OnUserFunction := HandleUdf;
Sheet := FBook.Sheets[1]; // gli indici dei worksheet partono da 1
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Atteso: Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106 e FAuditLog.Count = 0
end;
Il codice nativo -106 è la costante pubblica lxErrorUnsafeFunctionDenied, e Eval.Issue.Message legge "The formula callback is blocked by the safety policy". xlfeUnsafeFunctionDenied è l'ultimo membro di TXLSFormulaEvaluationStatus, dopo xlfeFailed. Codice che mappa gli stati con un case e un ramo else per il fallimento generico archivierà un rifiuto sotto «valutazione fallita» a meno che non nomini lo stato esplicitamente, e per una pipeline di upload è proprio quell'esito che vale la pena riportare a chi ha caricato il file
Default del workbook contro TXLSFormulaEvaluationOptions per chiamata
Il flag del workbook imposta il default e l'opzione per chiamata ha l'ultima parola. TXLSWorkbook.AllowUnsafeFormulaCallbacks e TXLSXWorkbook.AllowUnsafeFormulaCallbacks partono da False e governano il ricalcolo ordinario, Calculate, la EvaluateFormulaAt a tre argomenti, EvaluateAt su un evaluation template, le formule degli elenchi di data validation, le viste di sola lettura e, su XLSX, ogni worker nel pool di ricalcolo parallelo. Qualsiasi punto di ingresso che prende un record TXLSFormulaEvaluationOptions esplicito usa Options.AllowUnsafeFormulaCallbacks come verdetto per quella chiamata e non lo mette in OR con la proprietà del workbook
Quella asimmetria è deliberata, ed è il default giusto. Un processo interno fidato può autorizzare una ricerca RTD senza ribaltare l'intero workbook, e un workbook consentito globalmente può comunque forzare una valutazione sensata al rifiuto. XLSDefaultFormulaEvaluationOptions restituisce il flag come False, quindi costruire il record da quella funzione significa rifiuto, a meno che non lo impostiate di proposito. Un solo percorso ignora del tutto il flag: quando le regole di conditional formatting Classic vengono valutate per il rendering, i callback vengono negati qualunque cosa dica il workbook
var
Sheet: TXLSWorksheet;
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
Sheet := FBook.Sheets[1];
// il workbook resta bloccato, una sola chiamata fidata viene fatta passare
FBook.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// il workbook ha acconsentito, ma questa valutazione di testo caricato no
FBook.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // il flag è di nuovo False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message); // LogRejected è una routine vostra
end;
Cambiare la proprietà del workbook segna inoltre come sporca ogni formula nel grafo delle dipendenze, su entrambe le classi di workbook. Senza di ciò, un valore messo in cache mentre i callback erano consentiti potrebbe essere servito dopo che sono stati revocati, o un rifiuto in cache potrebbe sopravvivere a un opt-in. Impostare la proprietà al valore che ha già è un no-op e lascia in pace la cache, quindi alternarla dentro un ciclo costa un ricalcolo completo solo quando il valore cambia davvero
Che cosa succede a testo di formule sconosciute e unsafe al salvataggio?
Conservare una formula ed eseguirla sono due domande separate, e la policy di entrata risponde solo alla prima. FormulaEntryPolicy su entrambe le classi di workbook porta UnknownFunctionMode e UnknownNameMode, entrambi con default xlfusmReject. Assegnare una formula con una chiamata sconosciuta tramite la normale proprietà Formula solleva EArgumentException con "Formula entry rejected: unknown function" prima che il valore della cella, la cache delle formule o le dipendenze cambino. ValidateFormulaEntry restituisce la stessa decisione come record, senza effetti collaterali. I percorsi fidati come caricare un file, copiare e la conversione di formato non passano per la policy di entrata utente, perché un default severo non deve mai rifiutare simboli già presenti in un file che state solo aprendo
var
Sheet: TXLSWorksheet;
Policy: TXLSFormulaEntryPolicy;
Check: TXLSFormulaEntryValidationResult;
begin
Sheet := FBook.Sheets[1];
Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
// policy di default, nessun handler per ACME_RATE:
// Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject
Policy := FBook.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // entrata per compatibilità
FBook.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // conservata, non autorizzata
FBook.SaveAs('rates.xls');
end;
Il BIFF8 classico non ha un token per un nome di funzione arbitrario, quindi HotXLS scrive una chiamata sconosciuta nel modo in cui Excel scrive le funzioni add-in. La formula riceve un token PtgNameX ($59 la cui voce XTI punta al SUPBOOK degli add-in con entrambi gli indici di foglio impostati a $FFFE, seguita dai token degli argomenti e da un PtgFuncVar che porta il numero di funzione 255 e un conteggio di argomenti che include lo slot del nome. Il corpo ExternName che sta dietro è di sei byte a zero, un byte di lunghezza e il flag Unicode, il nome della funzione in UTF-16, poi una formula di due byte $1C $17, un PtgErr che contiene #REF!. Lo scrittore accetta nomi da 1 a 255 caratteri con al massimo 29 argomenti, e mira solo al BIFF8; un salvataggio BIFF5 non può portare la chiamata. Come queste voci SUPBOOK degli add-in stanno accanto ai collegamenti a workbook esterni è spiegato in le regole di classificazione SUPBOOK e XTI per i collegamenti esterni BIFF
XLSX conserva il testo grezzo della funzione e ODS conserva la sua formula msoxl:. In tutti e tre i formati un file che ha salvato =WEBSERVICE(...) si riapre con il nome intatto, e la sua valutazione predefinita restituisce ancora xlfeUnsafeFunctionDenied senza chiamare l'host. Il salvataggio non promuove mai i permessi di una formula: lo stesso testo rifiutato prima del round trip è rifiutato dopo
Per un server che ricalcola workbook non scritti da lui, la configurazione che regge è breve: lasciate AllowUnsafeFormulaCallbacks a False, registrate le funzioni per nome invece di assegnare un evento catch-all, e passate opzioni per chiamata solo dove il testo della formula viene dal vostro codice. Il riferimento a callback, policy di entrata e valutazione viaggia con la documentazione dell'HotXLS Delphi spreadsheet component