Technischer Artikel

HotXLS Unsafe-Callbacks: CALL und WEBSERVICE abschotten

HotXLS weigert sich, 20 gefährliche Formelnamen – darunter CALL, REGISTER.ID, WEBSERVICE und DDE – an Ihre Delphi-User-Function-Callbacks durchzureichen, sofern Sie nicht ausdrücklich zustimmen. Die Workbook-Property AllowUnsafeFormulaCallbacks steht per Default auf False, die Prüfung läuft, bevor irgendein Argument ausgewertet wird, und ein abgelehnter Aufruf meldet xlfeUnsafeFunctionDenied, ohne auch nur einen Handler aufzurufen

Das Szenario, das das nötig machte, ist profan. Ein Service nimmt hochgeladene XLS- oder XLSX-Dateien an, berechnet sie serverseitig neu und liest ein paar Summen zurück. Die Host-Anwendung hatte vor Jahren einen OnUserFunction-Handler für ein paar Business-Funktionen registriert, und irgendwann war in diesem Handler ein Catch-all-Zweig gewachsen, der alles Unbekannte an eine Plugin-Tabelle weiterreicht. Niemand im Team hat jemals =WEBSERVICE(...) in eine Zelle getippt. Der Uploader schon. Diese Formel beim Öffnen, Neuberechnen und Speichern intakt zu halten, ist ein Datei-Fidelity-Feature. Sie an Host-Code gelangen zu lassen, der Sockets oder Dateien öffnen kann, ist eine Autorisierungsentscheidung – und solange HotXLS die beiden Dinge nicht getrennt hatte, traf die Bibliothek diese Entscheidung stillschweigend für Sie

Warum wurde das Bewahren einer Formel zur Erlaubnis, sie auszuführen?

Die Ursache war ein einziger Fallback-Pfad. HotXLS parst jeden Excel-Funktionsnamen, den es kennt, aber nicht jeder bekannte Name hat eine Implementierung in der Calculation Engine. Erkannte, aber nicht implementierte Built-ins landeten früher im selben User-Defined-Function-Fallback wie echt eigene Namen, sodass CALL und REGISTER.ID denselben Dispatch-Pfad nutzten wie Ihr DISCOUNT oder REGIONRATE. Unbekannte Namen wie WEBSERVICE oder DDE konnten entsprechend auf einen gleichnamigen Eintrag in der Workbook-Registry, der prozessweiten Registry oder einem Event-Handler passen. Die Mechanik dieses Fallbacks erklärt der Artikel wie HotXLS eigene Funktionen über OnUserFunction auflöst; das Problem war, dass nichts auf diesem Pfad fragte, ob der Name selbst einer ist, den ein vernünftiger Host je ausführen sollte

Die Dispatch-Reihenfolge ist relevant dafür, was hier „unbekannt“ heißt. Ein Aufruf, den die Engine nicht nativ auswerten kann, wird der Reihe nach lexikalischen LAMBDA- und LET-Bindings angeboten, die die Closure-Unterstützung in der HotXLS-Formel-Engine zuerst auflöst, dann Funktionen auf Workbook-Ebene, die mit RegisterUserFunction registriert sind, dann prozessweite Funktionen aus TXLSWorkbook.RegisterGlobalUserFunction und schließlich den Events OnUserFunction und OnUserFunctionEx. Erst wenn alle ablehnen, wird eine wirklich unbekannte Funktion zu #NAME?. Jede Stufe nach dem Lambda-Lookup übergibt die Kontrolle an Code, den Sie geschrieben haben – genau deshalb muss die Sicherheitsprüfung vor die gesamte Kette statt in irgendeinen einzelnen Handler

Wie HotXLS unsichere Formula-Callbacks abriegelt: GetValueItemUserFunction prüft den Namen, bevor das Argument-Array gebaut wird oder irgendein Resolver läuft, sodass ein verschachteltes =WEBSERVICE(AUDIT_TOKEN()) mit lxErrorUnsafeFunctionDenied abbricht und das Audit-Log leer bleibt, während sichere Aufrufe die Kette von LAMBDA- und LET-Bindings hinab bis OnUserFunction durchlaufen
Erst wenn jede Stufe ablehnt, wird eine wirklich unbekannte Funktion zu #NAME?, weshalb die Sicherheitsprüfung vor die gesamte Kette statt in irgendeinen einzelnen Handler gehört

Welche Funktionsnamen blockiert HotXLS per Default?

XLSFormulaCallbackIsUnsafe in lxCalc.pas hält ein festes Deny-Set aus 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 und FILE.DELETE. Das sind die Namen, die in Excel oder seiner Makrosprache nativen Code laden, das Netzwerk erreichen, mit anderen Prozessen reden oder das Dateisystem anfassen. Vor dem Vergleich trimmt die Funktion umgebenden Whitespace, setzt den Namen in Großbuchstaben und entfernt ein einzelnes _XLFN.- oder _XLWS.-Präfix, sodass _xlfn.webservice aus einem neueren Excel-Build genauso erwischt wird wie die nackte Schreibweise. Die Liste lebt an der Calculator-Grenze statt in den Classic-, XLSX- und ODS-Parsern, was einen AST, einen BIFF-Tokenstream und eine konvertierte Arbeitsmappe identisch verhalten lässt

Wie HotXLS einen Funktionsnamen vor dem Unsafe-Callback-Vergleich normalisiert: Whitespace wird getrimmt, der Name in Großbuchstaben gesetzt und ein einzelnes _XLFN.- oder _XLWS.-Präfix entfernt, sodass _xlfn.webservice wie die nackte Schreibweise erwischt wird, danach wird das Ergebnis exakt gegen das feste Deny-Set aus 20 Namen in lxCalc.pas gematcht
Die Liste umfasst die Namen, die nativen Code laden, das Netzwerk erreichen, mit anderen Prozessen reden oder das Dateisystem anfassen, von DDE, CALL und WEBSERVICE bis FWRITE und FILE.DELETE

Zwei Randfälle lohnen sich zu kennen, bevor Sie sich darauf verlassen. Der Match ist exakt, ein Handler, den Sie als MYWEBSERVICE registrieren, ist also nicht betroffen, und umgekehrt wird eine legitime hauseigene UDF, die zufällig OPEN oder RUN heißt, jetzt per Default abgelehnt. Das Deny-Set ist auch keine Sandbox für Ihre eigenen Handler. Wenn Ihr Catch-all-Zweig beliebige Plugin-Namen ausführt, stoppt das Gate die berüchtigt gefährlichen und sonst nichts; die dauerhafte Lösung bleibt ein Handler, der gegen eine explizite Allowlist mit SameText matcht und Handled für alles, was ihm nicht gehört, auf False lässt

Warum muss das Gate vor der Argumentauswertung laufen?

Ein Gate, das erst feuert, nachdem die Argumente berechnet sind, kommt zu spät, denn die Argumente selbst können Ihren Code aufrufen. GetValueItemUserFunction prüft zuerst den Namen und steigt mit lxErrorUnsafeFunctionDenied aus, bevor es das Argument-Array baut, bevor es einen Resolver oder eine der beiden Registries konsultiert, und sogar, bevor es bemerkt, dass überhaupt kein Handler zugewiesen ist. Genau diese Reihenfolge vereitelt den folgenden verschachtelten Fall, in dem der äußere Aufruf ohnehin abgelehnt würde, aber eine harmlos aussehende innere UDF sonst zuerst feuern und ihren Seiteneffekt hinterlassen würde

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');   // Seiteneffekt im Host-Code
    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, und FAuditLog ist weiterhin leer

Workbook-Default gegen das per-call TXLSFormulaEvaluationOptions

Das Workbook-Flag ist der Default, die Option pro Aufruf hat das letzte Wort. TXLSWorkbook.AllowUnsafeFormulaCallbacks und TXLSXWorkbook.AllowUnsafeFormulaCallbacks regeln die gewöhnliche Neuberechnung, Calculate, das zweiparametrige EvaluateFormulaAt, Evaluations-Templates, Read-only-Views und auf XLSX jeden Worker im parallelen Neuberechnungspool. Jeder Einstiegspunkt, der einen expliziten TXLSFormulaEvaluationOptions-Record entgegennimmt, nimmt Options.AllowUnsafeFormulaCallbacks als Urteil für diesen Aufruf und verknüpft es nicht per ODER mit der Workbook-Property. Diese Asymmetrie ist Absicht: Ein vertrauter interner Job kann einen einzelnen RTD-Lookup autorisieren, ohne die ganze Arbeitsmappe umzuschalten, und eine global geöffnete Arbeitsmappe kann eine sensible Auswertung trotzdem zurück aufs Ablehnen zwingen

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // Arbeitsmappe bleibt abgeriegelt, ein vertrauter Aufruf kommt durch
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // Arbeitsmappe opted in, aber diese Auswertung von hochgeladenem Text nicht
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // Flag ist wieder False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Das Umschalten der Workbook-Property markiert den Dependency-Graph auf beiden Engines zusätzlich als dirty. Ohne diesen Schritt könnte ein gecachtes Ergebnis, das berechnet wurde, als Callbacks erlaubt waren, noch ausgeliefert werden, nachdem sie entzogen wurden, oder ein gecachtes xlfeUnsafeFunctionDenied-Ergebnis könnte ein Opt-in überdauern. Der neue Status wurde an TXLSFormulaEvaluationStatus hinter xlfeFailed angehängt, hat also die Ordinalzahl 10, und jede bestehende Ordinalzahl behält ihren Wert; dieselbe Tail-Append-Regel gilt für das Feld des Options-Records und den Getter und Setter von IXLSWorkbook, wobei ein gegen eine ältere Version gebauter Consumer weiterhin ein Recompile braucht

Was passiert mit unbekanntem und unsicherem Formeltext beim Speichern?

Eine Formel zu behalten und sie auszuführen sind jetzt zwei getrennte Fragen, und die Entry-Policy beantwortet nur die erste. FormulaEntryPolicy auf beiden Workbook-Klassen trägt UnknownFunctionMode und UnknownNameMode, beide mit Default xlfusmReject, sodass die Zuweisung einer Formel mit unbekanntem Aufruf über die normale Formula-Property abgelehnt wird, bevor sich Zellwert, Formula-Cache oder Dependencies ändern. ValidateFormulaEntry meldet dieselbe Entscheidung ohne Seiteneffekte. Vertrauenspfade wie Datei-Laden, Kopieren und Formatkonvertierung umgehen diese User-Entry-Policy, denn ein strikter Default darf niemals Symbole zurückweisen, die bereits in einer Datei stecken, die man bloß öffnet

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // Kompatibilitäts-Eintrag
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // gespeichert, nicht autorisiert
  Book.SaveAs('rates.xls');
end;

In Classic BIFF8 hat ein unbekannter Aufruf kein eigenes Token, also schreibt HotXLS ihn so, wie Excel Add-in-Funktionen schreibt. Die Formel bekommt ein PtgNameX-Token ($59), dessen XTI-Eintrag auf das Add-in-SUPBOOK zeigt, mit beiden Sheet-Indizes auf $FFFE gesetzt, gefolgt von den Argument-Tokens und einem PtgFuncVar mit Funktionsnummer 255 und einer Argumentanzahl, die den Namensslot mitzählt. Der zugrunde liegende ExternName-Body besteht aus sechs Null-Bytes, einem Längenbyte und Unicode-Flag, dem UTF-16-Funktionsnamen und dann einer Zwei-Byte-Formel aus $1C $17, einem PtgErr mit #REF!. Der Writer verweigert Namen länger als 255 Zeichen, mehr als 29 Argumente und das BIFF5-Ziel. Wie HotXLS diese Add-in-SUPBOOK-Einträge neben externen Workbook-Links einordnet, erklären die SUPBOOK- und XTI-Klassifizierungsregeln für BIFF-externe Links. XLSX behält den rohen Funktionstext und ODS seine msoxl:-Formel, und in jedem Format öffnet sich eine Datei, die =WEBSERVICE(...) gespeichert hat, wieder mit intaktem Text und evaluiert per Default weiterhin zu xlfeUnsafeFunctionDenied

Wie HotXLS einen unbekannten Formelaufruf ins klassische BIFF8 schreibt: Die Formel trägt ein PtgNameX-Token, dessen XTI-Eintrag auf das Add-in-SUPBOOK zeigt, mit beiden Sheet-Indizes $FFFE, dazu die Argument-Tokens und ein PtgFuncVar mit Funktionsnummer 255, gestützt von einem ExternName-Body, der in einer Zwei-Byte-$1C-$17-PtgErr mit #REF! endet
XLSX behält den rohen Funktionstext und ODS seine msoxl:-Formel, sodass eine Datei, die =WEBSERVICE(...) gespeichert hat, mit intaktem Text wieder aufgeht und per Default weiterhin zu xlfeUnsafeFunctionDenied evaluiert

Wenn Ihre Pipeline Arbeitsmappen auswertet, die sie nicht selbst verfasst hat, lassen Sie AllowUnsafeFormulaCallbacks auf False, halten Sie Handler auf einer expliziten Allowlist und gewähren Sie Optionen pro Aufruf nur dort, wo die Formelquelle Ihre eigene ist. Die komplette Callback-, Entry-Policy- und Evaluations-API ist bei der HotXLS-Delphi-Tabellenkomponente dokumentiert