Technischer Artikel

HotXLS Unsafe-Callbacks: CALL und WEBSERVICE abschotten

Tabellenkalkulations-Engines werten Argumente aus, bevor sie die Funktion aufrufen, die sie empfängt. Eine Zelle mit =WEBSERVICE(AUDIT_TOKEN()) führt also zuerst AUDIT_TOKEN aus, und wenn die Host-Anwendung AUDIT_TOKEN über einen User-Defined-Function-Handler beantwortet, hat dieser Handler seine Log-Zeile bereits geschrieben, seinen Zähler erhöht oder seine Verbindung geöffnet, noch bevor irgendetwas das Wort WEBSERVICE betrachtet. Eine Blockliste im Handler kommt zu spät. Genauso eine, die den äußeren Namen erst prüft, nachdem die Argumente zurückgekommen sind. Die Ablehnung muss über den äußeren Namen entschieden werden, bevor irgendein Argument angefasst wird und bevor irgendein Code, den der Host geschrieben hat, an die Reihe kommt

Das Szenario, in dem das beißt, ist banal. Ein Dienst nimmt hochgeladene XLS- oder XLSX-Dateien an, rechnet sie auf dem Server neu und liest ein paar Summen zurück. Vor Jahren hat jemand einen User-Function-Handler für zwei Geschäftsfunktionen registriert, und unterwegs ist ein Catch-all-Zweig daran gewachsen, der jeden unbekannten Namen an eine Plugin-Tabelle weiterreicht. Niemand im Team hat jemals =WEBSERVICE(...) in eine Zelle getippt. Der Uploader schon. Diese Formel durch Open, Recalc und Save hindurch intakt zu halten, ist eine Aufgabe der Datei-Treue; sie an Host-Code gelangen zu lassen, der Sockets öffnen oder Dateien schreiben kann, ist eine Autorisierungsentscheidung, und die beiden brauchen unterschiedliche Eigentümer

Wie eine bewahrte Formel Ihre Delphi-Callbacks erreicht

In der HotXLS-Formel-Engine fällt jeder Aufruf, den der Kalkulator nicht nativ auswerten kann, in eine gemeinsame Dispatch-Kette, und gefährliche Namen erreichen sie auf zwei Straßen. Die eine Straße sind eingebaute Funktionen, die der Parser erkennt, für die der Kalkulator aber keine Implementierung hat: CALL und REGISTER.ID sind echte Excel-Funktionsnamen, sie parsen also sauber und landen dann im selben Fallback wie Ihr DISCOUNT oder REGIONRATE. Die andere Straße sind Namen, die der Parser überhaupt nicht kennt, etwa WEBSERVICE oder DDE, die auf einen gleichnamigen Eintrag in einer Registry passen oder von einem Catch-all-Event-Handler beansprucht werden können. Die Mechanik dieses Fallbacks ist unter wie HotXLS Custom Functions über OnUserFunction auflöst beschrieben

Die Reihenfolge der Kette entscheidet, was „unbekannt“ bedeutet. Ein Name geht zuerst in die LAMBDA-Maschinerie: eine durch LET gebundene Closure oder ein definierter Name, dessen Formel ein LAMBDA ist, beides beschrieben in der Closure-Unterstützung der HotXLS-Formel-Engine. Danach kommen workbook-lokale Funktionen, registriert mit RegisterUserFunction, dann prozessweite Funktionen aus der Klassenmethode TXLSWorkbook.RegisterGlobalUserFunction, und zuletzt die Ereignisse. OnUserFunctionEx läuft, wenn es zugewiesen ist; OnUserFunction läuft nur, wenn OnUserFunctionEx fehlt. Ein wirklich unbekannter Name wird #NAME?, sobald jede Stufe abgelehnt hat, während ein erkannter, aber nicht implementierter Built-in eine Meldung über eine nicht unterstützte Funktion ausgibt

Jede Stufe nach dem LAMBDA-Lookup reicht die Kontrolle an Code weiter, den Sie geschrieben haben. Deshalb setzt HotXLS seine Sicherheitsprüfung vor jede Stufe, die Ihren Code erreicht, statt in eine einzelne davon, und deshalb sitzt die Prüfung im Kalkulator selbst statt in den drei Datei-Readern. Eine Formel, geladen aus Classic-BIFF-Tokens, aus XLSX-Text oder aus einer ODS-msoxl:-Formel, endet als derselbe geparste Ausdruck, also deckt eine Prüfung alle drei Formate ab, und eine konvertierte Mappe verhält sich genauso wie das Original

Wie HotXLS unsichere Formel-Callbacks absperrt: Der Funktionsname wird gegen das Deny-Set geprüft, bevor das Argument-Array gebaut wird oder ein Resolver läuft, also wird ein verschachteltes =WEBSERVICE(AUDIT_TOKEN()) mit xlfeUnsafeFunctionDenied abgelehnt und das Audit-Log bleibt leer, während sichere Aufrufe die Kette von LAMBDA- und LET-Bindungen hinunter zu OnUserFunction durchlaufen
Erst wenn jede Stufe ablehnt, wird eine wirklich unbekannte Funktion zu #NAME?, weshalb die Sicherheitsprüfung vor den Callback-Stufen sitzt statt innerhalb irgendeines Handlers

Welche Formelnamen lehnt HotXLS standardmäßig ab?

HotXLS lehnt einen festen Satz aus 20 Namen ab, solange Callbacks nicht ausdrücklich erlaubt sind: 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. In Excel oder seiner Makrosprache sind das die Namen, die nativen Code laden, das Netzwerk erreichen, mit anderen Prozessen reden oder das Dateisystem anfassen. Vor dem Vergleich trimmt die Engine umgebenden Whitespace, setzt den Namen in Großbuchstaben und entfernt ein einzelnes _XLFN.- oder _XLWS.-Präfix. Eine Datei, die ein neueres Excel-Build mit _xlfn.webservice geschrieben hat, wird genau wie die nackte Schreibweise erwischt

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

Der Vergleich ist exakt, was in beide Richtungen schneidet. Ein Handler, den Sie als MYWEBSERVICE registrieren, bleibt unberührt. Eine hauseigene UDF, die zufällig OPEN oder RUN heißt, wird standardmäßig abgelehnt, und der verlockende Fix ist, AllowUnsafeFormulaCallbacks auf der Mappe auf True zu setzen, damit dieser eine Name wieder funktioniert. Benennen Sie die Funktion stattdessen um. Die ganze Mappe freizuschalten, um einen einzigen hauseigenen Namen zu retten, reaktiviert auch CALL, DDE und FILE.DELETE für jede Formel in dieser Datei, inklusive Formeln, die Sie nicht geschrieben haben

Das Deny-Set zäunt auch nichts um Ihre eigenen Handler herum. Wenn ein Catch-all-Zweig beliebige Plugin-Namen ausführt, stoppt das Gate die 20 bekannten gefährlichen Namen und lässt alles andere zu Ihrem Code durch. Die dauerhafte Antwort ist, keine Catch-all-Handler mehr zu schreiben. Jede Funktion namentlich mit RegisterUserFunction zu registrieren macht die Registry selbst zur Allowlist, und es hält die Formel-Eintragsprüfung sinnvoll: Wenn OnUserFunction oder OnUserFunctionEx zugewiesen ist, kann die Engine nicht wissen, welche Namen das Ereignis beansprucht, also markiert die Eintragsvalidierung unbekannte Funktionsnamen überhaupt nicht mehr

procedure TImportService.WireFunctions;
begin
  // Eine Registrierung pro Name: die Registry ist die Allowlist
  FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
  FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
  // Beide Ereignisse unzugewiesen lassen, damit unbekannte Namen weiterhin
  // am Eintrag abgelehnt werden und bei der Auswertung als #NAME? enden
  FBook.OnUserFunction := nil;
  FBook.OnUserFunctionEx := nil;
end;

procedure TImportService.HandleDiscount(Sender: TObject;
  const FunctionName: WideString; const Args: Variant;
  var Value: Variant; var Handled: Boolean);
begin
  // Args ist ein nullbasiertes Variant-Array bereits ausgewerteter Argumente
  if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
  begin
    Value := Args[0] * (1 - Args[1]);
    Handled := True;
  end;
end;

Warum muss das Gate vor der Argumentauswertung laufen?

HotXLS prüft zuerst den Funktionsnamen und lehnt den Aufruf ab, bevor es das Argument-Array baut, bevor es einen Resolver oder eine der Registries konsultiert, und sogar bevor es bemerkt, dass überhaupt kein Handler zugewiesen ist. Genau diese Reihenfolge besiegt die verschachtelte Formel aus dem Einstieg: Das äußere WEBSERVICE wird abgelehnt, während AUDIT_TOKEN noch ein unausgewerteter Kindknoten ist, also läuft der Host-Handler nie. Der letzte Teil der Reihenfolge ist für das Logging wichtig. Eine Mappe ohne Handler und eine Mappe mit fünf melden für =WEBSERVICE(...) denselben Status, also ändert ein Ablehnungs-Log nicht seine Bedeutung, wenn später jemand einen Handler anbindet

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');   // Nebeneffekt im Host-Code
    Value := 'token-42';
    Handled := True;
  end;
end;

procedure TImportService.ProbeNestedCall;
var
  Sheet: TXLSWorksheet;
  Eval: TXLSFormulaEvaluationResult;
begin
  FBook.OnUserFunction := HandleUdf;
  Sheet := FBook.Sheets[1];                  // Worksheet-Indizes starten bei 1
  Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
  // Erwartet: Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
  // Eval.Issue.NativeCode = -106 und FAuditLog.Count = 0
end;

Der native Code -106 ist die öffentliche Konstante lxErrorUnsafeFunctionDenied, und Eval.Issue.Message lautet „The formula callback is blocked by the safety policy“. xlfeUnsafeFunctionDenied ist das letzte Mitglied von TXLSFormulaEvaluationStatus, nach xlfeFailed. Code, der Statuswerte mit einem case und einem else-Zweig für allgemeines Scheitern abbildet, verbucht eine Ablehnung unter „Evaluation fehlgeschlagen“, sofern er den Status nicht ausdrücklich benennt, und für eine Upload-Pipeline ist das genau die eine Rückmeldung, die es wert ist, an den Uploader weitergegeben zu werden

Workbook-Default gegen das per-call TXLSFormulaEvaluationOptions

Das Workbook-Flag setzt den Default, und die per-call-Option hat das letzte Wort. TXLSWorkbook.AllowUnsafeFormulaCallbacks und TXLSXWorkbook.AllowUnsafeFormulaCallbacks starten auf False und regieren die gewöhnliche Neuberechnung, Calculate, das EvaluateFormulaAt mit drei Argumenten, EvaluateAt auf einer Evaluations-Vorlage, Formeln von Datenvalidierungslisten, schreibgeschützte Ansichten und bei XLSX jeden Worker im Pool für parallele Neuberechnung. Jeder Einstiegspunkt, der einen expliziten TXLSFormulaEvaluationOptions-Record nimmt, verwendet Options.AllowUnsafeFormulaCallbacks als Entscheidung für diesen Aufruf und verknüpft sie nicht per OR mit der Workbook-Eigenschaft

Diese Asymmetrie ist Absicht, und sie ist der richtige Default. Ein vertrauter interner Job kann ein einzelnes RTD-Lookup autorisieren, ohne die ganze Mappe umzuklappen, und eine global freigeschaltete Mappe kann trotzdem eine sensible Auswertung auf Ablehnung zwingen. XLSDefaultFormulaEvaluationOptions liefert das Flag als False, also bedeutet das Bauen des Records aus dieser Funktion Ablehnung, sofern Sie es nicht bewusst setzen. Ein Pfad ignoriert das Flag vollständig: Wenn Classic-Regeln für bedingte Formatierung fürs Rendering ausgewertet werden, sind Callbacks abgelehnt, was auch immer die Mappe sagt

var
  Sheet: TXLSWorksheet;
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  Sheet := FBook.Sheets[1];

  // die Mappe bleibt abgeschlossen, ein vertrauter Aufruf kommt durch
  FBook.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // die Mappe freigeschaltet, aber diese Auswertung von hochgeladenem Text nicht
  FBook.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // das Flag ist wieder False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);   // LogRejected ist Ihre eigene Routine
end;

Das Ändern der Workbook-Eigenschaft markiert außerdem jede Formel im Abhängigkeitsgraphen als dirty, in beiden Workbook-Klassen. Ohne das könnte ein Wert, der zwischengespeichert wurde, als Callbacks noch erlaubt waren, bedient werden, nachdem sie widerrufen wurden, oder eine zwischengespeicherte Ablehnung könnte eine Freischaltung überleben. Die Eigenschaft auf den Wert zu setzen, den sie bereits hält, ist ein No-Op und lässt den Cache in Ruhe, also kostet das Umschalten in einer Schleife nur dann eine volle Neuberechnung, wenn sich der Wert tatsächlich ändert

Was passiert mit unbekanntem und unsicherem Formeltext beim Speichern?

Eine Formel zu bewahren und sie laufen zu lassen sind zwei getrennte Fragen, und die Eintragspolitik beantwortet nur die erste. FormulaEntryPolicy auf beiden Workbook-Klassen trägt UnknownFunctionMode und UnknownNameMode, beide mit Default xlfusmReject. Eine Formel mit unbekanntem Aufruf über die normale Formula-Eigenschaft zuzuweisen wirft eine EArgumentException mit „Formula entry rejected: unknown function“, bevor sich Zellenwert, Formel-Cache oder Abhängigkeiten ändern. ValidateFormulaEntry liefert dieselbe Entscheidung als Record, ohne Nebeneffekte. Vertraute Pfade wie das Laden einer Datei, Kopieren und Formatkonvertierung laufen nicht durch die Benutzer-Eintragspolitik, denn ein strenger Default darf niemals Symbole ablehnen, die bereits in einer Datei vorhanden sind, die Sie nur öffnen

var
  Sheet: TXLSWorksheet;
  Policy: TXLSFormulaEntryPolicy;
  Check: TXLSFormulaEntryValidationResult;
begin
  Sheet := FBook.Sheets[1];
  Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
  // Default-Politik, kein Handler für ACME_RATE:
  // Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject

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

Classic BIFF8 hat kein Token für einen beliebigen Funktionsnamen, also schreibt HotXLS einen unbekannten Aufruf 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, gefolgt von den Argument-Tokens und einem PtgFuncVar mit Funktionsnummer 255 und einer Argumentanzahl, die den Namens-Slot mitzählt. Der zugrunde liegende ExternName-Body besteht aus sechs Null-Bytes, einem Längenbyte und dem Unicode-Flag, dem UTF-16-Funktionsnamen, dann einer Zwei-Byte-Formel aus $1C $17, einem PtgErr, der #REF! hält. Der Writer akzeptiert Namen von 1 bis 255 Zeichen mit höchstens 29 Argumenten, und er zielt nur auf BIFF8; ein BIFF5-Speichern kann den Aufruf nicht tragen. Wie diese Add-in-SUPBOOK-Einträge neben externen Arbeitsmappen-Links sitzen, erklärt die SUPBOOK- und XTI-Klassifizierungsregeln für BIFF-externe Links

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

XLSX behält den rohen Funktionstext und ODS seine msoxl:-Formel. In allen drei Formaten geht eine Datei, die =WEBSERVICE(...) gespeichert hat, mit intaktem Namen wieder auf, und ihre Default-Auswertung liefert weiterhin xlfeUnsafeFunctionDenied, ohne den Host aufzurufen. Speichern hebt die Berechtigungen einer Formel nie an: Derselbe Text, der vor dem Roundtrip abgelehnt wurde, wird danach abgelehnt

Für einen Server, der Mappen neu berechnet, die er nicht selbst verfasst hat, hält sich die belastbare Konfiguration kurz: AllowUnsafeFormulaCallbacks auf False lassen, Funktionen namentlich registrieren statt ein Catch-all-Ereignis zuzuweisen, und per-call-Optionen nur dort übergeben, wo der Formeltext aus Ihrem eigenen Code stammt. Die Referenz für Callbacks, Eintragspolitik und Auswertung kommt mit der Dokumentation der HotXLS-Delphi-Tabellenkalkulationskomponente