HotXLS odmawia przekazywania 20 niebezpiecznych nazw formuł, w tym CALL, REGISTER.ID, WEBSERVICE i DDE, do twoich callbacków funkcji użytkownika w Delphi, dopóki sam nie wyrazisz na to zgody. Właściwość skoroszytu AllowUnsafeFormulaCallbacks ma domyślnie False, sprawdzenie biegnie przed ewaluacją jakiegokolwiek argumentu, a odrzucone wywołanie raportuje xlfeUnsafeFunctionDenied, nie wywoławszy żadnego handlera
Scenariusz, który tego wymusił, jest prozaiczny. Serwis przyjmuje wgrane pliki XLS albo XLSX, przelicza je po stronie serwera i odczytuje z powrotem parę sum. Aplikacja gospodarza zarejestrowała handler OnUserFunction lata temu dla pary funkcji biznesowych, a gdzieś po drodze handler wyrósł sobie z gałęzi catch-all, która przekazuje wszystko, czego nie rozpoznaje, 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 funkcja wierności pliku. Dopuszczenie jej do kodu hosta, który potrafi otwierać sockety albo pliki, to decyzja autoryzacyjna — a dopóki HotXLS nie rozdzielił tych dwóch rzeczy, biblioteka cicho podejmowała ją za ciebie
Dlaczego zachowanie formuły zamieniło się w pozwolenie na jej uruchomienie?
Pierwszą przyczyną była pojedyncza ścieżka awaryjna. HotXLS parsuje każdą nazwę funkcji Excela, którą zna, ale nie każda znana nazwa ma implementację w silniku obliczeń. Wbudowane rozpoznane, lecz niezaimplementowane, wpadały dawniej do tego samego fallbacku funkcji zdefiniowanych przez użytkownika co naprawdę własne nazwy, więc CALL i REGISTER.ID dzieliły ścieżkę dispatchu z twoim DISCOUNT albo REGIONRATE. Nieznane nazwy jak WEBSERVICE czy DDE mogły z kolei trafić na wpis o tej samej nazwie w rejestrze skoroszytu, rejestrze całego procesu albo handlerze zdarzenia. Mechanikę tego fallbacku opisuje tekst o tym, jak HotXLS rozwiązuje funkcje własne przez OnUserFunction; problem w tym, że nic na tej ścieżce nie pytało, czy nazwa sama w sobie to coś, co przyzwoity host powinien kiedykolwiek wykonać
Kolejność dispatchu ma znaczenie dla tego, co „nieznane” znaczy w tym miejscu. Wywołanie, którego silnik nie umie policzyć natywnie, trafia po kolei do leksykalnych wiązań LAMBDA i LET, które najpierw rozwiązuje obsługa domknięć w silniku formuł HotXLS, potem do funkcji lokalnych skoroszytu zarejestrowanych przez RegisterUserFunction, dalej do funkcji całego procesu z TXLSWorkbook.RegisterGlobalUserFunction, a na końcu do zdarzeń OnUserFunction i OnUserFunctionEx. Dopiero gdy wszystkie odmówią, naprawdę nieznana funkcja staje się #NAME?. Każdy etap po wyszukaniu lambdy przekazuje sterowanie kodowi, który napisałeś — i właśnie dlatego sprawdzenie bezpieczeństwa musi siedzieć przed całym łańcuchem, a nie wewnątrz któregokolwiek handlera
Które nazwy funkcji HotXLS blokuje domyślnie?
XLSFormulaCallbackIsUnsafe w lxCalc.pas trzyma stały zestaw 20 zakazanych nazw: 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. To nazwy, które w Excelu albo w jego języku makr ładują kod natywny, sięgają sieci, rozmawiają z innymi procesami albo dotykają systemu plików. Przed porównaniem funkcja ucina białe znaki z brzegów, podnosi nazwę do wielkich liter i zdejmuje pojedynczy prefiks _XLFN. albo _XLWS., więc _xlfn.webservice zapisany przez nowszą buildę Excela zostaje złapany tak samo jak goła pisownia. Lista mieszka na granicy kalkulatora, a nie w parserach Classic, XLSX i ODS, dzięki czemu jeden AST, jeden strumień tokenów BIFF i jeden skonwertowany skoroszyt zachowują się identycznie
Dwa brzegi warto znać, zanim na tym polegniesz. Dopasowanie jest dokładne, więc handler zarejestrowany jako MYWEBSERVICE nie jest dotknięty, a odwrotnie — legalna firmowa UDF, która akurat nazywa się OPEN albo RUN, jest od teraz domyślnie odrzucana. Zestaw zakazów to też nie sandbox dla twoich własnych handlerów. Jeśli twoja gałąź catch-all wykonuje dowolne nazwy pluginów, bramka zatrzyma słynne niebezpieczne i nic poza tym; trwała poprawka to nadal handler, który dopasowuje do jawnej listy dozwolonych przez SameText i zostawia Handled na False dla wszystkiego, czego nie jest właścicielem
Dlaczego bramka musi ruszać przed ewaluacją argumentów?
Bramka, która odpala po policzeniu argumentów, jest za późno, bo argumenty same potrafią wywołać twój kod. GetValueItemUserFunction najpierw sprawdza nazwę i wychodzi z lxErrorUnsafeFunctionDenied, 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 podpięty. Ta kolejność rozbraja zagnieżdżony przypadek z przykładu poniżej: zewnętrzne wywołanie i tak zostałoby odrzucone, ale niewinnie wyglądająca wewnętrzna UDF odpaliłaby pierwsza i zostawiła po sobie skutek uboczny
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;
Book.OnUserFunction := HandleUdf;
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106, a FAuditLog wciąż jest pusty
Domyślne ustawienie skoroszytu kontra per-wywołaniowe TXLSFormulaEvaluationOptions
Flaga skoroszytu jest domyślna, a opcja per wywołanie ma ostatnie słowo. TXLSWorkbook.AllowUnsafeFormulaCallbacks i TXLSXWorkbook.AllowUnsafeFormulaCallbacks rządzą zwykłym przeliczaniem, Calculate, dwuargumentowym EvaluateFormulaAt, szablonami ewaluacji, 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: 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ć cofnięcie wrażliwej ewaluacji na odrzucenie
var
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
// skoroszyt zostaje zablokowany, jedno zaufane wywołanie przechodzi
Book.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
Book.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);
end;
Przełączenie właściwości skoroszytu oznacza przy okazji graf zależności jako brudny w obu silnikach. Bez tego kroku wynik z cache policzony, gdy callbacki były dozwolone, mógłby zostać podany już po ich cofnięciu, albo zapisany w cache wynik xlfeUnsafeFunctionDenied mógłby przeżyć opt-in. Nowy status został doklejony do TXLSFormulaEvaluationStatus za xlfeFailed, więc ma ordinal 10 i każdy istniejący ordinal zachowuje swoją wartość; ta sama reguła doklejania na ogon obowiązuje pole rekordu opcji oraz getter i setter IXLSWorkbook, choć konsument zbudowany przeciw starszej wersji i tak potrzebuje rekompilacji
Co się dzieje z nieznanym i niebezpiecznym tekstem formuły przy zapisie?
Trzymanie formuły i jej uruchamianie to od teraz dwa osobne pytania, a polityka wejścia odpowiada tylko na pierwsze. FormulaEntryPolicy na obu klasach skoroszytów niesie UnknownFunctionMode i UnknownNameMode, oba domyślnie xlfusmReject, więc przypisanie formuły z nieznanym wywołaniem przez zwykłą właściwość Formula zostaje odrzucone, zanim zmienią się wartość komórki, cache formuły albo zależności. ValidateFormulaEntry raportuje tę samą decyzję bez skutków ubocznych. Zaufane ścieżki, jak wczytywanie plików, kopiowanie i konwersja formatów, omijają tę politykę wejścia użytkownika, bo surowy domyślny stan nie może nigdy odrzucić symboli już obecnych w pliku, który po prostu otwierasz
var
Policy: TXLSFormulaEntryPolicy;
begin
Policy := Book.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // wejście dla zgodności
Book.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // zapisana, nieautoryzowana
Book.SaveAs('rates.xls');
end;
W klasycznym BIFF8 nieznane wywołanie nie ma własnego tokenu, więc HotXLS zapisuje je 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 odmawia nazw dłuższych niż 255 znaków, więcej niż 29 argumentów i celu BIFF5. Jak HotXLS klasyfikuje te wpisy dodatkowych SUPBOOK 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, ODS trzyma swoją formułę msoxl:, a w każdym formacie plik, który zapisał =WEBSERVICE(...), otwiera się ponownie z nietkniętym tekstem i domyślnie dalej ewaluowany jest na xlfeUnsafeFunctionDenied
Jeśli twój potok ewaluowałby skoroszyty, których sam nie napisał, zostaw AllowUnsafeFormulaCallbacks na False, trzymaj handlery na jawnej liście dozwolonych i przyznawaj opcje per wywołanie tylko tam, gdzie źródło formuły jest twoje. Pełne API callbacków, polityki wejścia i ewaluacji jest udokumentowane przy komponencie arkuszowym HotXLS dla Delphi