Articolo tecnico

HotXLS: callback di formule unsafe sotto controllo

HotXLS si rifiuta di instradare 20 nomi di formule pericolosi, fra cui CALL, REGISTER.ID, WEBSERVICE e DDE, alle vostre callback Delphi per funzioni utente, a meno che non lo autorizziate esplicitamente. La proprietà del workbook AllowUnsafeFormulaCallbacks vale False per default, il controllo gira prima che qualsiasi argomento venga valutato, e una chiamata respinta riporta xlfeUnsafeFunctionDenied senza invocare un solo handler

Lo scenario che ha reso necessario tutto ciò è banale. Un servizio accetta file XLS o XLSX caricati, li ricalcola lato server e rilegge qualche totale. L'applicazione ospite aveva registrato anni prima un handler OnUserFunction per un paio di funzioni di business, e a un certo punto quell'handler si era portato dietro un ramo catch-all che inoltra a una tabella di plugin tutto ciò che non riconosce. Nessuno del team ha mai digitato =WEBSERVICE(...) in una cella. L'uploader sì. Conservare quella formula intatta attraverso open, ricalcolo e salvataggio è una funzionalità di fedeltà del file. Lasciarla arrivare a codice ospite che può aprire socket o file è una decisione di autorizzazione, e finché HotXLS non ha separato le due cose, era la libreria a prendere quella decisione in silenzio per conto vostro

Come mai preservare una formula è diventato permesso di eseguirla?

La causa radice era un unico percorso di fallback. HotXLS parsa ogni nome di funzione Excel che conosce, ma non ogni nome noto ha un'implementazione nel motore di calcolo. I built-in riconosciuti ma non implementati finivano nello stesso fallback di funzioni definite dall'utente dei nomi davvero custom, così CALL e REGISTER.ID condividevano un percorso di dispatch con la vostra DISCOUNT o REGIONRATE. I nomi sconosciuti come WEBSERVICE o DDE potevano a loro volta combaciare con una voce omonima nel registro del workbook, nel registro a livello di processo o in un event handler. La meccanica di quel fallback è trattata in come HotXLS risolve le funzioni custom tramite OnUserFunction; il problema era che nulla su quel percorso si chiedeva se il nome in sé fosse uno che un host sano dovrebbe mai eseguire

L'ordine di dispatch conta per ciò che "sconosciuto" significa qui. Una chiamata che l'engine non sa valutare nativamente viene offerta in sequenza ai binding lessicali LAMBDA e LET, che il supporto alle closure nel motore di formule HotXLS risolve per primi, poi alle funzioni locali del workbook registrate con RegisterUserFunction, poi alle funzioni a livello di processo di TXLSWorkbook.RegisterGlobalUserFunction, e infine agli eventi OnUserFunction e OnUserFunctionEx. Solo quando tutti declinano una funzione davvero sconosciuta diventa #NAME?. Ogni stadio dopo la ricerca delle lambda passa il controllo a codice scritto da voi, ed è esattamente per questo che il controllo di sicurezza deve stare davanti all'intera catena invece che dentro un singolo handler

Come HotXLS mette sotto controllo le callback di formule unsafe: GetValueItemUserFunction verifica il nome prima che l'array degli argomenti venga costruito o qualsiasi resolver giri, così una =WEBSERVICE(AUDIT_TOKEN()) nidificata esce con lxErrorUnsafeFunctionDenied e il log di audit resta vuoto, mentre le chiamate sicure percorrono la catena dai binding LAMBDA e LET fino a OnUserFunction
Solo quando ogni stadio declina una funzione davvero sconosciuta diventa #NAME?, ed è per questo che il controllo di sicurezza sta davanti all'intera catena invece che dentro un singolo handler

Quali nomi di funzioni blocca HotXLS per default?

XLSFormulaCallbackIsUnsafe in lxCalc.pas contiene un insieme di divieto fisso di 20 nomi: 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. Sono i nomi che, in Excel o nel suo linguaggio macro, caricano codice nativo, raggiungono la rete, parlano con altri processi o toccano il file system. Prima del confronto la funzione taglia gli spazi ai bordi, mette il nome in maiuscolo e stacca un eventuale prefisso singolo _XLFN. o _XLWS., così _xlfn.webservice scritto da una build Excel più recente viene beccato esattamente come la forma nuda. La lista vive al confine del calcolatore invece che nei parser Classic, XLSX e ODS, il che tiene un AST, uno stream di token BIFF e un workbook convertito a comportarsi in modo identico

Come HotXLS normalizza un nome di funzione prima del confronto con la lista unsafe-callback: gli spazi ai bordi vengono tagliati, il nome va in maiuscolo e un eventuale prefisso singolo _XLFN. o _XLWS. viene staccato così _xlfn.webservice viene beccato come la forma nuda, poi il risultato viene confrontato esattamente con l'insieme di divieto fisso di 20 nomi in lxCalc.pas
La lista copre i nomi che caricano codice nativo, raggiungono la rete, parlano con altri processi o toccano il file system, da DDE, CALL e WEBSERVICE fino a FWRITE e FILE.DELETE

Due finezze valgono prima di fare affidamento su tutto ciò. Il match è esatto, quindi un handler che registrate come MYWEBSERVICE non è toccato, e viceversa una legittima UDF interna che per caso si chiama OPEN o RUN da ora viene respinta per default. L'insieme di divieto nemmeno è una sandbox per i vostri handler. Se il vostro ramo catch-all esegue nomi di plugin arbitrari, il cancelletto ferma solo i pericolosi famosi e nient'altro; la correzione duratura resta un handler che matcha una allowlist esplicita con SameText e lascia Handled a False per tutto ciò che non gli appartiene

Perché il cancello deve girare prima della valutazione degli argomenti?

Un cancello che scatta dopo che gli argomenti sono stati calcolati arriva troppo tardi, perché sono gli argomenti stessi che possono chiamare il vostro codice. GetValueItemUserFunction verifica prima il nome ed esce con lxErrorUnsafeFunctionDenied prima di costruire l'array degli argomenti, prima di consultare un resolver o uno dei due registri, e perfino prima di accorgersi che non c'è alcun handler assegnato. Quell'ordinamento è ciò che sconfigge il caso nidificato qui sotto, dove la chiamata esterna sarebbe comunque rifiutata ma una UDF interna dall'aspetto innocuo sparerebbe per prima lasciandosi dietro il suo effetto collaterale

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 nel codice ospite
    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 FAuditLog resta ancora vuoto

Default del workbook contro TXLSFormulaEvaluationOptions per chiamata

Il flag del workbook è il default e l'opzione per chiamata ha l'ultima parola. TXLSWorkbook.AllowUnsafeFormulaCallbacks e TXLSXWorkbook.AllowUnsafeFormulaCallbacks governano il ricalcolo ordinario, Calculate, la EvaluateFormulaAt a due argomenti, i template di valutazione, le viste in sola lettura e, su XLSX, ogni worker del pool di ricalcolo parallelo. Qualsiasi punto d'ingresso che accetta un record TXLSFormulaEvaluationOptions esplicito prende Options.AllowUnsafeFormulaCallbacks come verdetto per quella chiamata e non lo mette in OR con la proprietà del workbook. Quell'asimmetria è voluta: un job interno fidato può autorizzare una singola ricerca RTD senza ribaltare l'intero workbook, e un workbook autorizzato globalmente può comunque forzare una valutazione sensibile al respingo

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // il workbook resta bloccato, una sola chiamata fidata passa
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // workbook autorizzato, ma questa valutazione di testo caricato no
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // il flag torna False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Cambiare la proprietà del workbook segna anche il grafo delle dipendenze come dirty su entrambi gli engine. Senza quel passo un risultato in cache calcolato mentre le callback erano autorizzate potrebbe essere servito dopo che sono state revocate, o un esito in cache xlfeUnsafeFunctionDenied potrebbe sopravvivere a un'autorizzazione. Il nuovo stato è stato aggiunto in coda a TXLSFormulaEvaluationStatus dopo xlfeFailed, quindi ha ordinale 10 e ogni ordinale esistente conserva il suo valore; la stessa regola di coda vale per il campo del record opzioni e per getter e setter di IXLSWorkbook, benché un consumer costruito contro una release precedente abbia comunque bisogno di una ricompilazione

Che cosa succede al testo delle formule sconosciute e unsafe al salvataggio?

Conservare una formula ed eseguirla sono ormai due domande separate, e la policy di ingresso risponde solo alla prima. FormulaEntryPolicy su ciascuna classe di workbook porta UnknownFunctionMode e UnknownNameMode, entrambi con default xlfusmReject, quindi assegnare una formula con una chiamata sconosciuta tramite la normale proprietà Formula viene respinto prima che il valore della cella, la cache delle formule o le dipendenze cambino. ValidateFormulaEntry riporta la stessa decisione senza effetti collaterali. I percorsi fidati come il caricamento dei file, la copia e la conversione di formato aggirano quella policy di ingresso utente, perché un default severo non deve mai respingere simboli già presenti in un file che state semplicemente aprendo

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // ingresso per compatibilità
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // memorizzata, non autorizzata
  Book.SaveAs('rates.xls');
end;

Nel BIFF8 classico una chiamata sconosciuta non ha un token proprio, quindi HotXLS la scrive 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 a $FFFE, seguito dai token degli argomenti e da un PtgFuncVar con numero di funzione 255 e un conteggio di argomenti che include lo slot del nome. Il corpo ExternName di supporto è 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!. Il writer rifiuta nomi più lunghi di 255 caratteri, più di 29 argomenti, e il target BIFF5. Come HotXLS classifica queste voci SUPBOOK di add-in 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:, e in ogni formato un file che ha salvato =WEBSERVICE(...) si riapre col testo intatto e valuta comunque a xlfeUnsafeFunctionDenied per default

Come HotXLS scrive una chiamata di formula sconosciuta nel BIFF8 classico: la formula porta un token PtgNameX la cui voce XTI punta al SUPBOOK degli add-in con entrambi gli indici di foglio $FFFE, poi i token degli argomenti e un PtgFuncVar con numero di funzione 255, supportati da un corpo ExternName che termina in un PtgErr di due byte $1C $17 contenente #REF!
XLSX conserva il testo grezzo della funzione e ODS conserva la sua formula msoxl:, così un file che ha salvato =WEBSERVICE(...) si riapre col testo intatto e valuta comunque a xlfeUnsafeFunctionDenied per default

Se la vostra pipeline valuta workbook che non ha scritto lei, lasciate AllowUnsafeFormulaCallbacks a False, tenete gli handler su una allowlist esplicita, e concedete le opzioni per chiamata solo dove la sorgente delle formule è vostra. L'API completa di callback, policy di ingresso e valutazione è documentata con il componente spreadsheet HotXLS per Delphi