Teknisk artikel

HotXLS usikre callbacks: gating af CALL og WEBSERVICE

HotXLS nægter at route 20 farlige formelnavne, blandt andet CALL, REGISTER.ID, WEBSERVICE og DDE, videre til dine Delphi user-function-callbacks, medmindre du selv slår det til. Arbejdsbogs-egenskaben AllowUnsafeFormulaCallbacks står som standard til False, tjekket kører, før noget argument evalueres, og et afvist kald rapporterer xlfeUnsafeFunctionDenied uden at involvere en eneste handler

Scenariet, der gjorde dette nødvendigt, er hverdagsagtigt. En service tager imod uploadede XLS- eller XLSX-filer, genberegner dem server-side og læser et par summer tilbage. Værtsapplikationen registrerede en OnUserFunction-handler for år siden til et par forretningsfunktioner, og på et tidspunkt fik den handler en catch-all-gren, der videresender alt, den ikke genkender, til en plugintabel. Ingen på teamet har nogensinde tastet =WEBSERVICE(...) ind i en celle. Uploaderen gjorde. At holde den formel intakt gennem open, recalc og save er en fil-trohed-funktion. At lade den nå værtskode, der kan åbne sockets eller filer, er en autorisationsbeslutning, og indtil HotXLS adskilte de to, tog biblioteket lydløst den beslutning på dine vegne

Hvorfor blev det at bevare en formel tilladelse til at køre den?

Rodårsagen var én enkelt fallback-sti. HotXLS parser alle Excel-funktionsnavne, den kender, men ikke alle kendte navne har en implementation i beregningsmotoren. Built-ins, der genkendes, men ikke er implementeret, landede tidligere i samme user-defined function-fallback som reelt custom-navne, så CALL og REGISTER.ID delte dispatch-sti med din DISCOUNT eller REGIONRATE. Ukendte navne som WEBSERVICE eller DDE kunne på samme måde matche et enslydende entry i arbejdsbogs-registret, det procesgobrede register eller en event handler. Mekanikken i den fallback er dækket i hvordan HotXLS resolver custom functions gennem OnUserFunction; problemet var, at intet på den sti spurgte, om navnet selv var et, en sund vært nogensinde burde eksekvere

Dispatch-rækkefølgen har betydning for, hvad "ukendt" betyder her. Et kald, motoren ikke kan evaluere nativt, tilbydes skiftevis til leksikale LAMBDA- og LET-bindings, som closure-supporten i HotXLS formelmotor resolver først, derefter til arbejdsbog-lokale funktioner registreret med RegisterUserFunction, derefter til procesgobrede funktioner fra TXLSWorkbook.RegisterGlobalUserFunction og til sidst til OnUserFunction- og OnUserFunctionEx-eventene. Først når alle afviser, bliver en reelt ukendt funktion til #NAME?. Hvert trin efter lambda-opslaget giver kontrollen videre til kode, du selv har skrevet, og det er præcis derfor, sikkerhedstjekket skal sidde foran hele kæden i stedet for inde i en vilkårlig handler

Hvordan HotXLS gater usikre formula-callbacks: GetValueItemUserFunction tjekker navnet, før argument-arrayet bygges eller nogen resolver kører, så et indlejret =WEBSERVICE(AUDIT_TOKEN()) afslutter med lxErrorUnsafeFunctionDenied og audit-loggen forbliver tom, mens sikre kald vandrer ad kæden fra LAMBDA- og LET-bindings ned til OnUserFunction
Først når hvert trin afviser, bliver en reelt ukendt funktion til #NAME?, og det er derfor, sikkerhedstjekket sidder foran hele kæden i stedet for inde i en vilkårlig handler

Hvilke funktionsnavne blokerer HotXLS som standard?

XLSFormulaCallbackIsUnsafe i lxCalc.pas holder et fast deny-sæt på 20 navne: 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 og FILE.DELETE. Det er navnene, der i Excel eller dets makrosprog indlæser native kode, når ud på netværket, taler med andre processer eller rører filsystemet. Før sammenligningen trimmer funktionen omgivende whitespace, konverterer navnet til versaler og fjerner ét enkelt _XLFN.- eller _XLWS.-præfiks, så _xlfn.webservice skrevet af en nyere Excel-build fanges ligesom den bare stavning. Listen ligger ved calculator-grænsen frem for i Classic-, XLSX- og ODS-parserne, hvilket holder én AST, én BIFF token stream og én konverteret arbejdsbog til at opføre sig ens

Hvordan HotXLS normaliserer et funktionsnavn før unsafe-callback-sammenligningen: whitespace trimmes, navnet konverteres til versaler og et enkelt _XLFN.- eller _XLWS.-præfiks fjernes, så _xlfn.webservice fanges ligesom den bare stavning, hvorefter resultatet matches eksakt mod det faste deny-sæt på 20 navne i lxCalc.pas
Listen spænder over navnene, der indlæser native kode, når ud på netværket, taler med andre processer eller rører filsystemet, fra DDE, CALL og WEBSERVICE til FWRITE og FILE.DELETE

To kanter er værd at kende, før du støtter dig til den. Matchet er eksakt, så en handler, du registrerer som MYWEBSERVICE, er ikke berørt, og omvendt bliver en legitim intern UDF, der tilfældigvis hedder OPEN eller RUN, nu afvist som standard. Deny-sættet er heller ikke en sandbox for dine egne handlers. Eksekverer din catch-all-gren vilkårlige plugin-navne, stopper gaten kun de berygtede farlige og intet andet; den holdbare fix er stadig en handler, der matcher en eksplicit allowlist med SameText og lader Handled stå på False for alt, den ikke ejer

Hvorfor skal gaten køre før argumentevaluering?

En gate, der udløses, efter argumenterne er beregnet, er for sent, fordi argumenterne selv kan kalde din kode. GetValueItemUserFunction tjekker navnet først og afslutter med lxErrorUnsafeFunctionDenied, før den bygger argument-arrayet, før den konsulterer en resolver eller et af registrene, og endda før den lægger mærke til, at der slet ikke er tildelt nogen handler. Den rækkefølge er det, der besejrer det indlejrede tilfælde nedenfor, hvor det ydre kald alligevel ville blive nægtet, men en harmløst udseende indre UDF ellers ville fyre først og efterlade sin sideeffekt

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');   // sideeffekt i værtskode
    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, og FAuditLog er stadig tom

Workbook-standard versus pr.-kald TXLSFormulaEvaluationOptions

Arbejdsbogs-flaget er standarden, og pr.-kald-tilvalget har det sidste ord. TXLSWorkbook.AllowUnsafeFormulaCallbacks og TXLSXWorkbook.AllowUnsafeFormulaCallbacks styrer almindelig genberegning, Calculate, den to-argumente EvaluateFormulaAt, evalueringsskabeloner, read-only views og på XLSX hver worker i den parallelle genberegningspool. Ethvert indgangspunkt, der accepterer en eksplicit TXLSFormulaEvaluationOptions-record, tager Options.AllowUnsafeFormulaCallbacks som dommen for det kald og OR'er den ikke med arbejdsbogs-egenskaben. Den asymmetri er bevidst: et betroet internt job kan godkende ét RTD-opslag uden at vende hele arbejdsbogen, og en arbejdsbog, der globalt er optet ind, kan stadig tvinge en sensitiv evaluering tilbage til afvisning

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // arbejdsbogen forbliver låst, ét betroet kald slipper igennem
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // arbejdsbogen optet ind, men denne evaluering af uploadet tekst er det ikke
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // flaget er False igen
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Skifter man arbejdsbogs-egenskaben, markeres afhængighedsgrafen også dirty i begge motorer. Uden det trin kunne et cached resultat, beregnet mens callbacks var tilladt, serveres efter, de blev tilbagekaldt, eller et cached xlfeUnsafeFunctionDenied-udfald kunne overleve en opt-in. Den nye status blev tilføjet til TXLSFormulaEvaluationStatus efter xlfeFailed, så den har ordinal 10, og alle eksisterende ordinals beholder deres værdi; samme tail-append-regel gælder options-record-feltet og IXLSWorkbook-getter og -setter, selvom en forbruger bygget mod en ældre udgivelse stadig skal rekompileres

Hvad sker der med ukendt og usikker formeltekst ved gemning?

At bevare en formel og at køre den er nu to adskilte spørgsmål, og entry-politikken besvarer kun det første. FormulaEntryPolicy på begge arbejdsbogsklasser bærer UnknownFunctionMode og UnknownNameMode, begge med standard xlfusmReject, så tildeling af en formel med et ukendt kald gennem den normale Formula-egenskab afvises, før celleværdien, formel-cachen eller afhængighederne ændres. ValidateFormulaEntry rapporterer samme beslutning uden sideeffekter. Betroede stier som filindlæsning, kopiering og formatkonvertering går uden om den bruger-entry-politik, fordi en streng standard aldrig må afvise symboler, der allerede er i en fil, man blot åbner

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // kompatibilitets-entry
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // gemt, ikke autoriseret
  Book.SaveAs('rates.xls');
end;

I Classic BIFF8 har et ukendt kald ingen token af egen, så HotXLS skriver det, som Excel skriver add-in-funktioner. Formlen får en PtgNameX-token ($59), hvis XTI-entry peger på add-in SUPBOOK med begge arkindekssæt sat til $FFFE, efterfulgt af argument-tokens og en PtgFuncVar med funktionsnummer 255 og et argumentantal, der inkluderer navnepladsen. Den bagvedliggende ExternName-krop er seks nul-bytes, en længdebyte og Unicode-flag, UTF-16-funktionsnavnet, derefter en to-byte formel bestående af $1C $17, en PtgErr med #REF!. Writeren nægter navne længere end 255 tegn, mere end 29 argumenter og BIFF5-målet. Hvordan HotXLS klassificerer disse add-in SUPBOOK-entries ved siden af eksterne arbejdsbogs-links, er forklaret i SUPBOOK- og XTI-klassificeringsreglerne for BIFF eksterne links. XLSX beholder den rå funktionstekst, og ODS beholder sin msoxl:-formel, og i alle formater genåbner en fil, der gemte =WEBSERVICE(...), med teksten intakt og evaluerer stadig til xlfeUnsafeFunctionDenied som standard

Hvordan HotXLS skriver et ukendt formelkald ind i klassisk BIFF8: formlen bærer en PtgNameX-token, hvis XTI-entry peger på add-in SUPBOOK med begge arkindekssæt $FFFE, derefter argument-tokens og en PtgFuncVar med funktionsnummer 255, baget af en ExternName-krop, der slutter med en to-byte $1C $17 PtgErr med #REF!
XLSX beholder den rå funktionstekst og ODS sin msoxl:-formel, så en fil, der gemte =WEBSERVICE(...), genåbner med teksten intakt og evaluerer stadig til xlfeUnsafeFunctionDenied som standard

Evaluerer din pipeline arbejdsbøger, den ikke selv har forfattet, så lad AllowUnsafeFormulaCallbacks stå på False, hold handlers på en eksplicit allowlist, og giv pr.-kald-tilvalg kun, hvor formelkilden er din egen. Det fulde callback-, entry-policy- og evaluerings-API er dokumenteret hos HotXLS Delphi spreadsheet component