Silniki arkuszowe liczą argumenty, zanim zawołają funkcję, która je dostaje. Komórka z =WEBSERVICE(AUDIT_TOKEN()) uruchamia więc najpierw AUDIT_TOKEN, i jeśli aplikacja gospodarza obsługuje AUDIT_TOKEN przez handler funkcji zdefiniowanych przez użytkownika, ten handler zdąży już zapisać swoją linijkę loga, podbić licznik albo otworzyć połączenie, zanim cokolwiek spojrzy na słowo WEBSERVICE. Blocklista wewnątrz handlera to za późno. Za późno jest też wtedy, gdy ktoś ogląda zewnętrzną nazwę po tym, jak argumenty wrócą. Odmowa musi być rozstrzygnięta na zewnętrznej nazwie, zanim dotknie się jakiegokolwiek argumentu i zanim jakikolwiek kod napisany przez hosta dostanie swoją kolejkę
Sytuacja, w której to gryzie, jest prozaiczna. Serwis przyjmuje wgrane pliki XLS albo XLSX, przelicza je na serwerze i odczytuje z powrotem parę sum. Lata temu ktoś zarejestrował handler funkcji użytkownika dla dwóch funkcji biznesowych, a po drodze wyrósł w nim catch-all przekazujący każdy nierozpoznany numer do tabeli pluginów. Nikt w zespole nigdy nie wpisał =WEBSERVICE(...) do komórki. Wgrał to ktoś z zewnątrz. Utrzymanie tej formuły nietkniętej przez otwarcie, przeliczenie i zapis to robota za wierność pliku; dopuszczenie jej do kodu hosta, który potrafi otwierać sockety albo pisać pliki, to decyzja autoryzacyjna — a te dwie rzeczy potrzebują różnych właścicieli
Jak zachowana formuła dociera do twoich callbacków Delphi
W silniku formuł HotXLS każde wywołanie, którego kalkulator nie umie policzyć natywnie, wpada do jednej wspólnej ścieżki dispatchu, a niebezpieczne nazwy przychodzą tam dwiema drogami. Jedna to wbudowane funkcje, które parser rozpoznaje, ale kalkulator nie ma dla nich implementacji: CALL i REGISTER.ID to prawdziwe nazwy funkcji Excela, więc parsują się czysto, po czym lądują w tym samym fallbacku co twój DISCOUNT albo REGIONRATE. Druga to nazwy, których parser w ogóle nie zna, jak WEBSERVICE albo DDE, które mogą trafić na wpis o tej samej nazwie w rejestrze albo zostać przechwycone przez handler zdarzenia catch-all. Mechanikę tego fallbacku opisuje tekst o tym, jak HotXLS rozwiązuje funkcje własne przez OnUserFunction
Kolejność łańcucha rozstrzyga, co znaczy „nieznane". Nazwa trafia najpierw do machinerii LAMBDA: domknięcie związane przez LET albo zdefiniowana nazwa, której formułą jest LAMBDA — oba opisane w obsłudze domknięć w silniku formuł HotXLS. Po niej przychodzą funkcje lokalne skoroszytu zarejestrowane przez RegisterUserFunction, dalej funkcje całego procesu z metody klasowej TXLSWorkbook.RegisterGlobalUserFunction, a na końcu zdarzenia. OnUserFunctionEx chodzi, gdy jest przypisane; OnUserFunction chodzi tylko, gdy OnUserFunctionEx nie jest. Naprawdę nieznana nazwa staje się #NAME?, gdy każdy etap odmówi, a rozpoznany, lecz niezaimplementowany wbudowany raportuje funkcję niewspieraną
Każdy etap po wyszukaniu LAMBDA przekazuje sterowanie kodowi, który napisałeś. Dlatego HotXLS stawia swoje sprawdzenie bezpieczeństwa przed każdym etapem, który dociera do twojego kodu, a nie wewnątrz któregokolwiek z nich, i dlatego sprawdzenie siedzi w samym kalkulatorze, a nie w trzech czytnikach plików. Formuła wczytana z tokenów Classic BIFF, z tekstu XLSX albo z formuły msoxl: w ODS kończy jako to samo sparsowane wyrażenie, więc jedno sprawdzenie pokrywa wszystkie trzy formaty, a skonwertowany skoroszyt zachowuje się tak samo jak oryginał
Które nazwy formuł HotXLS odrzuca domyślnie?
HotXLS odrzuca stały zestaw 20 nazw, chyba że callbacki zostały jawnie dozwolone: 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 i FILE.DELETE. W Excelu albo w jego języku makr to nazwy, które ładują kod natywny, sięgają sieci, rozmawiają z innymi procesami albo dotykają systemu plików. Przed porównaniem silnik ucina białe znaki z brzegów, podnosi nazwę do wielkich liter i zdejmuje pojedynczy prefiks _XLFN. albo _XLWS.. Plik zapisany przez nowszą buildę Excela, który trzyma _xlfn.webservice, zostaje złapany dokładnie tak samo jak goła pisownia
Dopasowanie jest dokładne, co tnie w obie strony. Handler zarejestrowany jako MYWEBSERVICE nie jest dotknięty. Firmowa UDF, która akurat nazywa się OPEN albo RUN, jest domyślnie odrzucana, a kusząca naprawa to ustawić AllowUnsafeFormulaCallbacks na True na skoroszycie, żeby ta jedna nazwa znów działała. Zamiast tego zmień nazwę funkcji. Włączenie całego skoroszytu, by uratować jedną firmową nazwę, włącza z powrotem CALL, DDE i FILE.DELETE dla każdej formuły w tym pliku, łącznie z formułami, których ty nie napisałeś
Zestaw zakazów nie ogrodzi też twoich własnych handlerów. Jeśli gałąź catch-all wykonuje dowolne nazwy pluginów, bramka zatrzyma 20 dobrze znanych niebezpiecznych i przepuści wszystko inne do twojego kodu. Trwała odpowiedź to przestać pisać handlery catch-all. Rejestracja każdej funkcji po nazwie przez RegisterUserFunction zamienia sam rejestr w listę dozwolonych i trzyma sprawdzenie wejścia formuły sensownym: gdy OnUserFunction albo OnUserFunctionEx jest przypisane, silnik nie umie wiedzieć, które nazwy zdarzenie sobie przechwyci, więc walidacja wejścia przestaje flagować nieznane nazwy funkcji w ogóle
procedure TImportService.WireFunctions;
begin
// Jedna rejestracja na nazwę: rejestr jest listą dozwolonych
FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
// Zostaw oba zdarzenia nieprzypisane, żeby nieznane nazwy wciąż
// były odrzucane na wejściu i kończyły jako #NAME? przy ewaluacji
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 to wariantowa tablica indeksowana od zera z już policzonych argumentów
if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
begin
Value := Args[0] * (1 - Args[1]);
Handled := True;
end;
end;
Dlaczego bramka musi ruszać przed ewaluacją argumentów?
HotXLS sprawdza najpierw nazwę funkcji i odmawia wywołania, zanim zbuduje tablicę argumentów, zanim sięgnie do rezolwera albo do któregokolwiek rejestru, i nawet zanim zauważy, że żaden handler w ogóle nie jest przypisany. Ta kolejność rozbraja zagnieżdżoną formułę z otwarcia artykułu: zewnętrzny WEBSERVICE zostaje odrzucony, póki AUDIT_TOKEN jest wciąż niepoliczonym węzłem dziecka, więc handler hosta nigdy nie chodzi. Ostatnia część kolejności ma znaczenie dla logowania. Skoroszyt bez handlerów i skoroszyt z pięcioma raportują ten sam status dla =WEBSERVICE(...), więc log odrzuceń nie zmienia znaczenia, gdy ktoś później podepnie handler
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'); // skutek uboczny w kodzie hosta
Value := 'token-42';
Handled := True;
end;
end;
procedure TImportService.ProbeNestedCall;
var
Sheet: TXLSWorksheet;
Eval: TXLSFormulaEvaluationResult;
begin
FBook.OnUserFunction := HandleUdf;
Sheet := FBook.Sheets[1]; // indeksy arkuszy startują od 1
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Oczekiwane: Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106 oraz FAuditLog.Count = 0
end;
Kod natywny -106 to publiczna stała lxErrorUnsafeFunctionDenied, a Eval.Issue.Message brzmi „The formula callback is blocked by the safety policy". xlfeUnsafeFunctionDenied jest ostatnim członem TXLSFormulaEvaluationStatus, za xlfeFailed. Kod mapujący statusy przez case z gałęzią else dla ogólnej porażki wpisałby odmowę pod „nieudana ewaluacja", chyba że jawnie nazwie status, a dla potoku wgrywania to właśnie ten wynik wart jest odesłania wgrywającemu
Domyślne ustawienie skoroszytu kontra per-wywołaniowe TXLSFormulaEvaluationOptions
Flaga skoroszytu ustawia domyślne, a opcja per wywołanie ma ostatnie słowo. TXLSWorkbook.AllowUnsafeFormulaCallbacks i TXLSXWorkbook.AllowUnsafeFormulaCallbacks startują jako False i rządzą zwykłym przeliczaniem, Calculate, trójargumentowym EvaluateFormulaAt, EvaluateAt na szablonie ewaluacji, formułami list walidacji danych, widokami tylko do odczytu i — po stronie XLSX — każdym workerem puli równoległego przeliczania. Każdy punkt wejścia przyjmujący jawny rekord TXLSFormulaEvaluationOptions bierze Options.AllowUnsafeFormulaCallbacks jako werdykt dla tego wywołania i nie skleja go operatorem OR z właściwością skoroszytu
Ta asymetria jest celowa i to jest właściwy domyślny. Zaufane wewnętrzne zadanie może zautoryzować jedno wyszukanie RTD bez przekręcania całego skoroszytu, a skoroszyt globalnie włączony potrafi mimo to wymusić powrót wrażliwej ewaluacji na odrzucenie. XLSDefaultFormulaEvaluationOptions zwraca flagę jako False, więc zbudowanie rekordu z tej funkcji znaczy odrzucenie, chyba że ustawisz ją celowo. Jedna ścieżka ignoruje flagę całkowicie: gdy reguły warunkowego formatowania Classic są liczone dla renderowania, callbacki są odrzucane, cokolwiek mówi skoroszyt
var
Sheet: TXLSWorksheet;
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
Sheet := FBook.Sheets[1];
// skoroszyt zostaje zablokowany, jedno zaufane wywołanie przechodzi
FBook.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// skoroszyt włączony, ale ta ewaluacja wgranego tekstu już nie
FBook.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // flaga znów ma False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message); // LogRejected to twoja własna rutyna
end;
Zmiana właściwości skoroszytu oznacza też przy okazji każdą formułę w grafie zależności jako brudną, na obu klasach skoroszytów. Bez tego wartość zcache'owana w czas, gdy callbacki były dozwolone, mogłaby zostać podana już po ich cofnięciu, albo zcache'owana odmowa mogłaby przeżyć opt-in. Ustawienie właściwości na wartość, którą już trzyma, jest no-opem i zostawia cache w spokoju, więc przełączanie jej w pętli kosztuje pełne przeliczenie tylko, gdy wartość faktycznie się zmienia
Co się dzieje z nieznanym i niebezpiecznym tekstem formuły przy zapisie?
Trzymanie formuły i jej uruchamianie to dwa osobne pytania, a polityka wejścia odpowiada tylko na pierwsze. FormulaEntryPolicy na każdej z klas skoroszytów niesie UnknownFunctionMode i UnknownNameMode, oba domyślnie xlfusmReject. Przypisanie formuły z nieznanym wywołaniem przez zwykłą właściwość Formula podnosi EArgumentException z komunikatem „Formula entry rejected: unknown function", zanim zmienią się wartość komórki, cache formuły albo zależności. ValidateFormulaEntry zwraca tę samą decyzję jako rekord, bez skutków ubocznych. Zaufane ścieżki, jak wczytywanie pliku, kopiowanie i konwersja formatów, nie przechodzą przez politykę wejścia użytkownika, bo surowy domyślny nie może nigdy odrzucić symboli już obecnych w pliku, który po prostu otwierasz
var
Sheet: TXLSWorksheet;
Policy: TXLSFormulaEntryPolicy;
Check: TXLSFormulaEntryValidationResult;
begin
Sheet := FBook.Sheets[1];
Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
// domyślna polityka, brak handlera dla ACME_RATE:
// Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject
Policy := FBook.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // wejście dla zgodności
FBook.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // zapisana, nieautoryzowana
FBook.SaveAs('rates.xls');
end;
Klasyczny BIFF8 nie ma tokenu na dowolną nazwę funkcji, więc HotXLS zapisuje nieznane wywołanie tak, jak Excel zapisuje funkcje dodatków. Formuła dostaje token PtgNameX ($59), którego wpis XTI wskazuje na dodatkowy SUPBOOK z oboma indeksami arkuszy ustawionymi na $FFFE, po nim idą tokeny argumentów i PtgFuncVar z numerem funkcji 255 i liczbą argumentów uwzględniającą slot nazwy. Wspierające ciało ExternName to sześć zerowych bajtów, bajt długości i flaga Unicode, nazwa funkcji w UTF-16, a potem dwubajtowa formuła $1C $17, czyli PtgErr niosący #REF!. Zapisujący przyjmuje nazwy od 1 do 255 znaków z najwyżej 29 argumentami i celuje wyłącznie w BIFF8; zapis BIFF5 nie uniesie tego wywołania. Jak te wpisy dodatkowych SUPBOOK mieszczą się obok zewnętrznych łączy do skoroszytów, wyjaśnia tekst o regułach klasyfikacji SUPBOOK i XTI dla zewnętrznych łączy BIFF
XLSX trzyma surowy tekst funkcji, a ODS swoją formułę msoxl:. We wszystkich trzech formatach plik, który zapisał =WEBSERVICE(...), otwiera się ponownie z nietkniętą nazwą, a jego domyślna ewaluacja nadal zwraca xlfeUnsafeFunctionDenied bez wołania hosta. Zapis nigdy nie podnosi uprawnień formuły: ten sam tekst, który był odrzucony przed rundą otwórz-zapisz, jest odrzucony po niej
Dla serwera przeliczającego skoroszyty, których sam nie napisał, konfiguracja, która się broni, jest krótka: zostaw AllowUnsafeFormulaCallbacks na False, rejestruj funkcje po nazwie zamiast przypisywać zdarzenie catch-all i podawaj opcje per wywołanie tylko tam, gdzie tekst formuły pochodzi z twojego kodu. Dokumentacja callbacków, polityki wejścia i ewaluacji przychodzi w komplecie z komponentem arkuszowym HotXLS dla Delphi