HotXLS odmítá routovat 20 nebezpečných názvů formulí, mezi nimi CALL, REGISTER.ID, WEBSERVICE a DDE, do vašich Delphi user-function callbacků, pokud si to explicitně nezapnete. Vlastnost sešitu AllowUnsafeFormulaCallbacks defaultně stojí na False, kontrola běží dřív, než se vyhodnotí jakýkoli argument, a odmítnuté volání nahlásí xlfeUnsafeFunctionDenied bez zavolání jediného handleru
Scénář, který tohle učinil nutným, je úplně obyčejný. Služba přijímá nahrané soubory XLS nebo XLSX, přepočítá je na serveru a přečte zpět pár součtů. Hostitelská aplikace si před lety zaregistrovala handler OnUserFunction pro pár business funkcí a někdy cestou ten handler dostal catch-all větev, která všechno, co nepozná, přeposílá do tabulky pluginů. Nikdo z týmu nikdy nenapsal =WEBSERVICE(...) do buňky. Nahrávač ano. Udržet ten vzorec neporušený skrz otevření, přepočet a uložení je funkce věrnosti souboru. Pustit ho k host kódu, který umí otevřít sockety nebo soubory, je autorizační rozhodnutí a dokud HotXLS obojí nerozdělil, dělala knihovna tohle rozhodnutí potichu za vás
Jak se ze zachování vzorce stalo povolení ho spustit?
Kořenem problému byla jediná fallback cesta. HotXLS parsuje každý název Excel funkce, který zná, ale ne každý známý název má implementaci v počítacím engine. Built-iny rozpoznané, ale neimplementované, dřív padaly do stejného user-defined fallbacku jako doopravdy vlastní názvy, takže CALL a REGISTER.ID sdílely dispatch cestu s vašimi DISCOUNT nebo REGIONRATE. Neznámé názvy jako WEBSERVICE nebo DDE mohly stejně tak sednout na stejnojmennou položku v registru sešitu, procesovém registru nebo event handleru. Mechaniku toho fallbacku rozebírá článek o tom, jak HotXLS rozlišuje vlastní funkce přes OnUserFunction; problém byl, že se nic na té cestě nezeptalo, jestli je název sám o sobě něco, co zdravý hostitel kdy má spustit
Dispatch pořadí má význam pro to, co zde „neznámé“ znamená. Volání, které engine neumí vyhodnotit nativně, se nabízí postupně lexikálním bindingům LAMBDA a LET, které rozliší nejdřív podpora closures ve formula engine HotXLS, pak funkcím lokálním pro sešit zaregistrovaným přes RegisterUserFunction, pak procesovým funkcím z TXLSWorkbook.RegisterGlobalUserFunction a nakonec eventům OnUserFunction a OnUserFunctionEx. Teprve když všechny odmítnou, stane se z doopravdy neznámé funkce #NAME?. Každý stupeň po lambda vyhledání předává kontrolu kódu, který jste napsali vy — a přesně proto musí bezpečnostní kontrola sedět před celým řetězem, ne uvnitř jednoho konkrétního handleru
Které názvy funkcí blokuje HotXLS defaultně?
XLSFormulaCallbackIsUnsafe v lxCalc.pas drží pevnou deny sadu 20 názvů: 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 a FILE.DELETE. Jsou to názvy, které v Excelu nebo jeho makro jazyce načítají nativní kód, sahnou po síti, mluví s jinými procesy nebo sahají na souborový systém. Před porovnáním funkce ořízne okolní whitespace, přehodí název na velká písmena a setře jedinou předponu _XLFN. nebo _XLWS., takže _xlfn.webservice napsané novějším buildem Excelu chytí stejně jako holý zápis. Seznam žije na hranici kalkulačky, ne v parserech Classic, XLSX a ODS, takže jeden AST, jeden BIFF token stream a jeden konvertovaný sešit se chovají identicky
Dva rohy stojí za to znát, než se na to spolehnete. Shoda je exaktní, takže handler registrovaný jako MYWEBSERVICE zůstane nedotčený a naopak legitimní interní UDF, která se náhodou jmenuje OPEN nebo RUN, je teď defaultně odmítaná. Deny sada taky není sandbox pro vaše vlastní handlery. Pokud vaše catch-all větev spouští libovolné názvy pluginů, brána zastaví ty slavné nebezpečné a nic jiného; trvalá oprava je i nadále handler, který porovnává explicitní allowlist přes SameText a nechává Handled na False pro všechno, co si nepřisvojuje
Proč musí brána běžet před vyhodnocením argumentů?
Brána, která vystřelí až po spočítání argumentů, je pozdě, protože samotné argumenty mohou zavolat váš kód. GetValueItemUserFunction zkontroluje název nejdřív a skončí s lxErrorUnsafeFunctionDenied, než postaví pole argumentů, než se zeptá resolveru nebo kteréhokoli registru, a dokonce dřív, než si vůbec všimne, že žádný handler není přiřazený. Právě tohle pořadí porazí vnořený případ níže, kde by vnější volání stejně bylo odmítnuté, ale neškodně vypadající vnitřní UDF by jinak vystřelila první a nechala za sebou svůj vedlejší efekt
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'); // vedlejší efekt v host kódu
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 a FAuditLog je pořád prázdný
Default sešitu versus per-call TXLSFormulaEvaluationOptions
Flag sešitu je default a per-call volba má poslední slovo. TXLSWorkbook.AllowUnsafeFormulaCallbacks a TXLSXWorkbook.AllowUnsafeFormulaCallbacks řídí obyčejný přepočet, Calculate, dvouargumentové EvaluateFormulaAt, evaluation šablony, read-only pohledy a na XLSX každého workera v paralelním přepočetním poolu. Každý vstupní bod přijímající explicitní záznam TXLSFormulaEvaluationOptions bere Options.AllowUnsafeFormulaCallbacks jako verdikt toho volání a neslučuje ho (OR) s vlastností sešitu. Ta asymetrie je záměrná: důvěryhodná interní úloha může autorizovat jedno RTD vyhledání bez přehození celého sešitu a globálně opt-in sešit může pořád donutit citlivé vyhodnocení zpátky k odmítnutí
var
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
// sešit zůstává zamknutý, jediné důvěryhodné volání se pustí
Book.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// sešit je opt-in, ale tohle vyhodnocení nahraného textu není
Book.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // flag je zase False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message);
end;
Přepnutí vlastnosti sešitu taky označí graf závislostí jako dirty na obou enginech. Bez toho kroku by mohl cachovaný výsledek spočítaný, když byly callbacky povolené, vyjít i poté, co byly odňaty, nebo cachovaný výstup xlfeUnsafeFunctionDenied mohl přežít opt-in. Nový status se přidal na konec TXLSFormulaEvaluationStatus za xlfeFailed, takže má ordinal 10 a každý existující ordinal si drží hodnotu; totéž pravidlo přidávání na konec platí pro pole options záznamu a getter a setter IXLSWorkbook, byť konzument builděný proti staršímu vydání potřebuje i tak rekompilaci
Co se s neznámým a nebezpečným textem vzorce stane při uložení?
Uchovat vzorec a spustit ho jsou teď dvě oddělené otázky a vstupní politika odpovídá jen na tu první. FormulaEntryPolicy na obou třídách sešitu nese UnknownFunctionMode a UnknownNameMode, obojí defaultně xlfusmReject, takže přiřazení vzorce s neznámým voláním přes obyčejnou vlastnost Formula se odmítne dřív, než se změní hodnota buňky, formula cache nebo závislosti. ValidateFormulaEntry nahlásí totéž rozhodnutí bez vedlejších efektů. Důvěryhodné cesty jako načítání souborů, kopírování a konverze formátů tu user-entry politiku obcházejí, protože přísný default nikdy nesmí odmítnout symboly už přítomné v souboru, který jen otvíráte
var
Policy: TXLSFormulaEntryPolicy;
begin
Policy := Book.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // kompatibilní vstup
Book.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // uloženo, ne autorizováno
Book.SaveAs('rates.xls');
end;
V Classic BIFF8 nemá neznámé volání vlastní token, takže HotXLS ho zapisuje tak, jak Excel zapisuje add-in funkce. Vzorec dostane token PtgNameX ($59), jehož XTI položka míří na add-in SUPBOOK s oběma indexy listů nastavenými na $FFFE, následují tokeny argumentů a PtgFuncVar nesoucí číslo funkce 255 a počet argumentů zahrnující slot názvu. Podkladové tělo ExternName je šest nulových bytů, byte délky a Unicode flag, UTF-16 název funkce a pak dvoubytový vzorec $1C $17, PtgErr držící #REF!. Writer odmítá názvy delší než 255 znaků, víc než 29 argumentů a cíl BIFF5. Jak HotXLS klasifikuje tyhle add-in položky SUPBOOK vedle externích odkazů na sešity, vysvětluje článek o pravidlech klasifikace SUPBOOK a XTI pro externí odkazy BIFF. XLSX nechává syrový text funkce a ODS svůj vzorec msoxl: a v každém formátu se soubor, který uložil =WEBSERVICE(...), znovu otevře s textem neporušeným a defaultně se stále vyhodnotí na xlfeUnsafeFunctionDenied
Pokud váš pipeline vyhodnocuje sešity, které sám nenapsal, nechte AllowUnsafeFormulaCallbacks na False, držte handlery na explicitním allowlistu a přiznávejte per-call volby jen tam, kde je zdroj vzorce váš. Kompletní API callbacků, vstupní politiky a vyhodnocování je zdokumentované u komponenty HotXLS pro tabulky v Delphi