Technisch artikel

HotXLS onveilige formule-callbacks: CALL en WEBSERVICE

HotXLS weigert 20 gevaarlijke formulenamen, waaronder CALL, REGISTER.ID, WEBSERVICE en DDE, door te sturen naar uw Delphi user-function-callbacks tenzij u zich expliciet aanmeldt. De werkboekeigenschap AllowUnsafeFormulaCallbacks staat standaard op False, de controle draait voordat er ook maar één argument wordt geëvalueerd, en een geweigerde aanroep meldt xlfeUnsafeFunctionDenied zonder ook maar één handler aan te roepen

Het scenario dat dit nodig maakte is alledaags. Een service accepteert geüploade XLS- of XLSX-bestanden, herberekent ze server-side en leest een paar totalen terug. De hostapplicatie registreerde jaren geleden een OnUserFunction-handler voor een handjevol bedrijfsfuncties, en ergens onderweg groeide die handler uit met een catch-all-tak die alles wat hij niet herkent doorstuurt naar een plugintabel. Niemand in het team heeft ooit =WEBSERVICE(...) in een cel getypt. De uploader wel. Die formule intact houden door open, herberekening en opslag is een bestandsgetrouwheidsfeature. Het doorlaten naar hostcode die sockets of bestanden kan openen is een autorisatiebesluit, en tot HotXLS de twee scheidde nam de library dat besluit geruisloos voor u

Hoe werd het bewaren van een formule toestemming om hem uit te voeren?

De oorzaak was één enkele fallback-route. HotXLS parseert elke Excel-functienaam die hij kent, maar niet elke bekende naam heeft een implementatie in de rekengine. Built-ins die herkend maar niet geïmplementeerd zijn, vielen vroeger in dezelfde user-defined-function-fallback als echt custom namen, dus CALL en REGISTER.ID deelden een dispatch-route met uw DISCOUNT of REGIONRATE. Onbekende namen zoals WEBSERVICE of DDE konden op hun beurt terechtkomen bij een gelijknamige invoer in het werkboekregister, het procesbrede register of een eventhandler. De werking van die fallback staat beschreven in hoe HotXLS custom functions oplost via OnUserFunction; het probleem was dat niets op die route vroeg of de naam zelf wel een is die een gezonde host ooit zou uitvoeren

De dispatchvolgorde bepaalt wat "onbekend" hier betekent. Een aanroep die de engine niet natively kan evalueren wordt achtereenvolgens aangeboden aan lexicale LAMBDA- en LET-bindings, die de closure-ondersteuning in de HotXLS-formule-engine als eerste oplost, daarna aan werkboeklokale functies geregistreerd met RegisterUserFunction, daarna aan procesbrede functies uit TXLSWorkbook.RegisterGlobalUserFunction, en tenslotte aan de events OnUserFunction en OnUserFunctionEx. Pas als ze allemaal weigeren wordt een echt onbekende functie #NAME?. Elke fase na de lambda-oplossing geeft de regels uit handen aan code die u zelf schreef, en dat is precies waarom de veiligheidscontrole vóór de hele keten moet zitten in plaats van ergens in één handler

Hoe HotXLS onveilige formule-callbacks afschermt: GetValueItemUserFunction controleert de naam voordat de argumentarray wordt gebouwd of een resolver draait, dus een geneste =WEBSERVICE(AUDIT_TOKEN()) eindigt met lxErrorUnsafeFunctionDenied en het auditlog blijft leeg, terwijl veilige aanroepen de keten aflopen van LAMBDA- en LET-bindings tot aan OnUserFunction
Pas als elke fase weigert wordt een echt onbekende functie #NAME?, en daarom zit de veiligheidscontrole vóór de hele keten in plaats van ergens in één handler

Welke functienamen blokkeert HotXLS standaard?

XLSFormulaCallbackIsUnsafe in lxCalc.pas houdt een vaste deny-set bij van 20 namen: 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 en FILE.DELETE. Het zijn de namen die in Excel of zijn macrotaal native code laden, het netwerk op gaan, met andere processen praten of het bestandssysteem aanraken. Vóór de vergelijking trimt de functie witruimte aan de randen, zet hij de naam om naar hoofdletters en stript hij één prefix _XLFN. of _XLWS., dus _xlfn.webservice geschreven door een nieuwere Excel-build wordt net zo gevangen als de kale spelling. De lijst leeft aan de calculatorgrens in plaats van in de Classic-, XLSX- en ODS-parsers, waardoor één AST, één BIFF-tokenstream en één geconverteerd werkboek identiek gedrag houden

Hoe HotXLS een functienaam normaliseert vóór de onveilige-callback-vergelijking: witruimte wordt getrimd, de naam wordt naar hoofdletters omgezet en één prefix _XLFN. of _XLWS. wordt gestript, zodat _xlfn.webservice net als de kale spelling wordt gevangen, waarna het resultaat exact wordt gematcht tegen de vaste deny-set van 20 namen in lxCalc.pas
De lijst bestrijkt de namen die native code laden, het netwerk op gaan, met andere processen praten of het bestandssysteem aanraken, van DDE, CALL en WEBSERVICE tot FWRITE en FILE.DELETE

Twee kanttekeningen zijn goed om te kennen voordat u erop vertrouwt. De match is exact, dus een handler die u als MYWEBSERVICE registreert wordt niet geraakt, en omgekeerd wordt een legitieme interne UDF die toevallig OPEN of RUN heet nu standaard geweigerd. De deny-set is ook geen sandbox voor uw eigen handlers. Als uw catch-all-tak willekeurige pluginnamen uitvoert, stopt de gate alleen de beruchte gevaarlijke en verder niets; de duurzame fix blijft een handler die met SameText matcht op een expliciete allowlist en Handled op False laat voor alles wat niet van hem is

Waarom moet de gate vóór de argumentevaluatie draaien?

Een gate die afgaat nadat de argumenten zijn berekend is te laat, want de argumenten zelf kunnen uw code aanroepen. GetValueItemUserFunction controleert eerst de naam en eindigt met lxErrorUnsafeFunctionDenied voordat hij de argumentarray bouwt, voordat hij een resolver of een van beide registers raadpleegt, en zelfs voordat hij merkt dat er helemaal geen handler is toegewezen. Juist die volgorde maakt de geneste casus hieronder onschadelijk, waar de buitenste aanroep toch geweigerd zou worden maar een onschuldig ogende innerlijke UDF anders eerst zou afgaan en zijn bijeffect zou achterlaten

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');   // bijeffect in hostcode
    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, en FAuditLog is nog steeds leeg

Werkboekstandaard versus per-aanroep TXLSFormulaEvaluationOptions

De werkboekflag is de standaard en de per-aanroep-optie heeft het laatste woord. TXLSWorkbook.AllowUnsafeFormulaCallbacks en TXLSXWorkbook.AllowUnsafeFormulaCallbacks regeren de gewone herberekening, Calculate, de EvaluateFormulaAt met twee argumenten, evaluatiesjablonen, alleen-lezen-weergaven en op XLSX elke worker in de parallelle herberekeningspool. Elk toegangspunt dat een expliciet record TXLSFormulaEvaluationOptions accepteert neemt Options.AllowUnsafeFormulaCallbacks als het oordeel voor die aanroep en voegt het niet met de werkboekeigenschap samen. Die asymmetrie is bewust: een vertrouwde interne taak kan één RTD-opzoeking autoriseren zonder het hele werkboek om te gooien, en een werkboek dat globaal is aangemeld kan een gevoelige evaluatie toch terugdwingen naar weigeren

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // werkboek blijft op slot, één vertrouwde aanroep komt erdoorheen
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // werkboek aangemeld, maar deze evaluatie van geüploade tekst niet
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // flag is weer False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Het omschakelen van de werkboekeigenschap markeert de dependency graph bovendien dirty op beide engines. Zonder die stap zou een gecachet resultaat berekend terwijl callbacks waren toegestaan nog na intrekking kunnen worden geserveerd, of zou een gecachte uitkomst xlfeUnsafeFunctionDenied een aanmelding kunnen overleven. De nieuwe status is achteraan toegevoegd aan TXLSFormulaEvaluationStatus na xlfeFailed, dus hij heeft ordinale waarde 10 en elke bestaande ordinale waarde behoudt zijn betekenis; dezelfde staarttoevoegregel geldt voor het veld van het options-record en de getter en setter van IXLSWorkbook, al blijft een consumer die tegen een oudere release is gebouwd een hercompilatie nodig hebben

Wat gebeurt er bij het opslaan met onbekende en onveilige formuletekst?

Een formule bewaren en hem uitvoeren zijn nu twee gescheiden vragen, en het invoerbeleid beantwoordt alleen de eerste. FormulaEntryPolicy op beide werkboekklassen draagt UnknownFunctionMode en UnknownNameMode, die beide standaard op xlfusmReject staan, dus het toewijzen van een formule met een onbekende aanroep via de gewone eigenschap Formula wordt geweigerd voordat de celwaarde, de formulecache of de afhankelijkheden veranderen. ValidateFormulaEntry meldt dezelfde beslissing zonder bijeffecten. Vertrouwde paden zoals bestanden laden, kopiëren en formaatconversie omzeilen dat gebruikersinvoerbeleid, want een strikte standaard mag nooit symbolen weigeren die al in een bestand zitten dat u slechts opent

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // compatibele invoer
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // opgeslagen, niet geautoriseerd
  Book.SaveAs('rates.xls');
end;

In Classic BIFF8 heeft een onbekende aanroep geen eigen token, dus HotXLS schrijft hem zoals Excel add-in-functies schrijft. De formule krijgt een token PtgNameX ($59) waarvan de XTI-invoer wijst naar de add-in-SUPBOOK met beide sheetindexen op $FFFE, gevolgd door de argumenttokens en een PtgFuncVar met functienummer 255 en een argumenttelling die de naamslot meerekent. De ondersteunende ExternName-body bestaat uit zes nulbytes, een lengtebyte en Unicode-vlag, de UTF-16-functienaam, daarna een formule van twee bytes $1C $17, een PtgErr met #REF!. De writer weigert namen langer dan 255 tekens, meer dan 29 argumenten en het BIFF5-doel. Hoe HotXLS deze add-in-SUPBOOK-invoeren indelt naast externe werkboeklinks legt de SUPBOOK- en XTI-indelingsregels voor BIFF-externe links uit. XLSX houdt de rauwe functietekst vast en ODS zijn msoxl:-formule, en in elk formaat heropent een bestand dat =WEBSERVICE(...) opslaat met de tekst intact en evalueert het standaard nog steeds tot xlfeUnsafeFunctionDenied

Hoe HotXLS een onbekende formuleaanroep in klassiek BIFF8 schrijft: de formule draagt een PtgNameX-token waarvan de XTI-invoer wijst naar de add-in-SUPBOOK met beide sheetindexen $FFFE, daarna de argumenttokens en een PtgFuncVar met functienummer 255, ondersteund door een ExternName-body die eindigt in een twee bytes grote $1C $17 PtgErr met #REF!
XLSX houdt de rauwe functietekst vast en ODS zijn msoxl:-formule, dus een bestand dat =WEBSERVICE(...) opslaat heropent met de tekst intact en evalueert standaard nog steeds tot xlfeUnsafeFunctionDenied

Als uw pipeline werkboeken evalueert die hij niet zelf schreef, laat AllowUnsafeFormulaCallbacks op False staan, houd handlers op een expliciete allowlist en verleen per-aanroep-opties alleen waar de formulebron van uzelf is. De volledige callback-, invoerbeleid- en evaluatie-API is gedocumenteerd bij de HotXLS Delphi spreadsheet-component