Tehnički članak

HotXLS nesigurni callbackovi: kapija za CALL i WEBSERVICE

HotXLS odbija usmjeriti 20 opasnih imena formula, među njima CALL, REGISTER.ID, WEBSERVICE i DDE, Vašim Delphi user-function callbackovima osim ako Vi to izričito uključite. Svojstvo radne knjige AllowUnsafeFormulaCallbacks zadano je False, provjera se izvršava prije evaluacije ijednog argumenta, a odbijeni poziv javlja xlfeUnsafeFunctionDenied ne pozvavši nijedan handler

Scenarij koji je ovo učinio nužnim svakidašnji je. Servis prima uploadane XLS ili XLSX datoteke, preračunava ih na serveru i čita natrag par zbrojeva. Host aplikacija registrirala je OnUserFunction handler prije godina za par poslovnih funkcija, i negdje uz put taj handler izrastao je u catch-all granu koja sve što ne prepoznaje prosljeđuje tablici plugina. Nitko u timu nikad nije utipkao =WEBSERVICE(...) u ćeliju. Uploader jest. Zadržati tu formulu netaknutom kroz otvaranje, preračun i spremanje značajka je vjernosti datoteke. Pustiti je do host koda koji može otvoriti sockete ili datoteke odluka je o ovlastima, i dok HotXLS nije odvojio jedno od drugog, biblioteka je tu odluku tiho donosila na Vaše ime

Zašto je čuvanje formule postalo dopuštenje za njeno izvršavanje?

Korijenski uzrok bila je jedna fallback putanja. HotXLS parsira svako ime Excel funkcije koje poznaje, ali ne svako poznato ime ima implementaciju u kalkulacijskom engineu. Ugrađena imena koja su prepoznata a neimplementirana padala su u isti user-defined function fallback kao istinski custom imena, pa su CALL i REGISTER.ID dijelili dispatch putanju s Vašim DISCOUNT ili REGIONRATE. Nepoznata imena poput WEBSERVICE ili DDE mogla su jednako tako poklopiti istoimeni unos u registru radne knjige, registru cijelog procesa ili event handleru. Mehanika tog fallbacka pokrivena je u kako HotXLS razrješuje custom funkcije kroz OnUserFunction; problem je bio što ništa na toj putanji nije pitalo je li ime samo jedno koje bi zdravi host trebao ikad izvršiti

Dispatch redoslijed bitan je za to što "nepoznato" ovdje znači. Poziv koji engine ne može izvorno evaluirati nudi se redom leksičkim LAMBDA i LET vezama, koje prve razrješuje podrška closurea u HotXLS formula engineu, zatim funkcijama lokalnim za radnu knjigu registriranim s RegisterUserFunction, onda funkcijama cijelog procesa iz TXLSWorkbook.RegisterGlobalUserFunction, i na kraju eventima OnUserFunction i OnUserFunctionEx. Tek kad svi odbiju, istinski nepoznata funkcija postaje #NAME?. Svaka faza nakon lambda pretrage predaje kontrolu kodu koji ste napisali, i baš zato sigurnosna provjera mora stajati ispred cijelog lanca, a ne unutar bilo kojeg pojedinog handlera

Kako HotXLS kapiju postavlja na nesigurne formula callbackove: GetValueItemUserFunction provjerava ime prije nego se izgradi polje argumenata ili pokrene bilo koji resolver, pa ugniježđeni =WEBSERVICE(AUDIT_TOKEN()) izlazi s lxErrorUnsafeFunctionDenied i audit log ostaje prazan, dok sigurni pozivi prelaze lanac od LAMBDA i LET veza sve do OnUserFunction
Tek kad svaka faza odbije, istinski nepoznata funkcija postaje #NAME?, zato sigurnosna provjera stoji ispred cijelog lanca, a ne unutar bilo kojeg pojedinog handlera

Koja imena funkcija HotXLS blokira po defaultu?

XLSFormulaCallbackIsUnsafe u lxCalc.pas drži fiksni deny skup od 20 imena: 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 su imena koja, u Excelu ili njegovom makro jeziku, učitavaju nativni kod, izlaze na mrežu, razgovaraju s drugim procesima ili diraju datotečni sustav. Prije usporedbe funkcija odreže okolni whitespace, nametne imenu velika slova i skine jedan prefiks _XLFN. ili _XLWS., pa se _xlfn.webservice koji ga napiše novija Excel verzija hvata jednako kao golo pisanje. Popis živi na granici kalkulatora umjesto u Classic, XLSX i ODS parserima, što jednom AST-u, jednom BIFF tokenu streamu i jednoj konvertiranoj radnoj knjici ostavlja identično ponašanje

Kako HotXLS normalizira ime funkcije prije nesigurno-callback usporedbe: whitespace se odreže, ime dobiva velika slova i skida se jedan prefiks _XLFN. ili _XLWS. pa se _xlfn.webservice uhvati kao golo pisanje, a zatim se rezultat točno uspoređuje s fiksnim deny skupom od 20 imena u lxCalc.pas
Popis obuhvaća imena koja učitavaju nativni kod, izlaze na mrežu, razgovaraju s drugim procesima ili diraju datotečni sustav, od DDE, CALL i WEBSERVICE do FWRITE i FILE.DELETE

Dvije su rubne situacije vrijedne znanja prije nego se na to oslonite. Poklapanje je točno, pa handler koji registrirate kao MYWEBSERVICE nije pogođen, i obrnuto, legitimna kućna UDF koja se slučajno zove OPEN ili RUN sada je po defaultu odbijena. Deny skup također nije sandbox za Vaše vlastite handlere. Ako Vaša catch-all grana izvršava proizvoljna imena plugina, kapija zaustavi poznate opasne i ništa više; trajni popravak i dalje je handler koji se poklapa s eksplicitnim allowlistom putem SameText i ostavlja Handled na False za sve što ne posjeduje

Zašto kapija mora raditi prije evaluacije argumenata?

Kapija koja opali nakon što su argumenti izračunati kasni, jer argumenti sami mogu zvati Vaš kod. GetValueItemUserFunction najprije provjerava ime i izlazi s lxErrorUnsafeFunctionDenied prije nego izgradi polje argumenata, prije nego konzultira resolver ili bilo koji registar, i čak prije nego uoči da nijedan handler uopće nije dodijeljen. Taj redoslijed je ono što obara ugniježđeni slučaj dolje, gdje bi vanjski poziv ionako bio odbijen, ali bi bez toga bezopasno izgledajuća unutarnja UDF opalila prva i ostavila svoj side effect za sobom

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');   // side effect u host kodu
    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, i FAuditLog je i dalje prazan

Default radne knjige u odnosu na TXLSFormulaEvaluationOptions po pozivu

Zastavica radne knjige je default, a opcija po pozivu zadnja je riječ. TXLSWorkbook.AllowUnsafeFormulaCallbacks i TXLSXWorkbook.AllowUnsafeFormulaCallbacks upravljaju običnim preračunavanjem, Calculate, dvargumentnim EvaluateFormulaAt, evaluacijskim predlošcima, read-only prikazima i, na XLSX, svakim workerom u paralelnom preračunskom poolu. Svaki ulaz koji prima eksplicitni zapis TXLSFormulaEvaluationOptions uzima Options.AllowUnsafeFormulaCallbacks kao presudu za taj poziv i ne spaja ga OR-om sa svojstvom radne knjige. Ta je asimetrija namjerna: pouzdani interni posao može ovlastiti jedno RTD traženje bez prevrtanja cijele radne knjige, a radna knjiga globalno uključena i dalje može osjetljivu evaluaciju vratiti na odbijanje

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // radna knjiga ostaje zaključana, jedan pouzdani poziv prolazi
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // radna knjiga uključena, ali ova evaluacija uploadanog teksta nije
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // zastavica je opet False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Prebacivanje svojstva radne knjige također označava graf ovisnosti prljavim na oba enginea. Bez tog koraka keširani rezultat izračunat dok su callbackovi bili dopušteni mogao bi se poslužiti nakon što su opozvani, ili keširani ishod xlfeUnsafeFunctionDenied mogao bi nadživjeti uključenje. Novi status dodan je na TXLSFormulaEvaluationStatus nakon xlfeFailed, pa ima redni broj 10 i svaki postojeći redni broj zadržava svoju vrijednost; isto pravilo dodavanja na rep vrijedi za polje zapisa opcija te getter i setter IXLSWorkbook, iako potrošač građen protiv starijeg izdanja i dalje treba rekompilaciju

Što se s nepoznatim i nesigurnim formulama događa pri spremanju?

Čuvati formulu i izvršiti je sada su dva odvojena pitanja, i entry politika odgovara samo na prvo. FormulaEntryPolicy na bilo kojoj klasi radne knjige nosi UnknownFunctionMode i UnknownNameMode, oba zadano xlfusmReject, pa se dodjela formule s nepoznatim pozivom kroz normalno svojstvo Formula odbija prije nego se vrijednost ćelije, formula cache ili ovisnosti promijene. ValidateFormulaEntry javlja istu odluku bez side effecta. Pouzdani putevi poput učitavanja datoteke, kopiranja i konverzije formata zaobilaze tu user-entry politiku, jer strogi default nikad ne smije odbiti simbole koji već postoje u datoteci koju Vi samo otvarate

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // unos radi kompatibilnosti
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // spremljeno, ne ovlašteno
  Book.SaveAs('rates.xls');
end;

U Classic BIFF8 nepoznati poziv nema vlastiti token, pa ga HotXLS piše onako kako Excel piše add-in funkcije. Formula dobiva token PtgNameX ($59) čiji XTI unos pokazuje na add-in SUPBOOK s oba indeksa listova postavljenima na $FFFE, nakon kojeg slijede tokeni argumenata i PtgFuncVar s brojem funkcije 255 i brojačem argumenata koji uključuje slot imena. Podupiruće tijelo ExternName je šest nula bajtova, bajt duljine i Unicode zastavica, UTF-16 ime funkcije, zatim dvobajtna formula $1C $17, PtgErr koji drži #REF!. Pisac odbija imena dulja od 255 znakova, više od 29 argumenata i BIFF5 cilj. Kako HotXLS klasificira te add-in SUPBOOK unose uz vanjske veze radnih knjiga objašnjeno je u SUPBOOK i XTI pravilima klasifikacije za BIFF vanjske veze. XLSX čuva sirovi tekst formule, ODS svoju msoxl: formulu, i u svakom formatu datoteka koja je spremila =WEBSERVICE(...) otvara se s tekstom netaknutim i i dalje evaluira u xlfeUnsafeFunctionDenied po defaultu

Kako HotXLS upisuje nepoznati poziv formule u klasični BIFF8: formula nosi token PtgNameX čiji XTI unos pokazuje na add-in SUPBOOK s oba indeksa listova $FFFE, zatim tokene argumenata i PtgFuncVar s brojem funkcije 255, poduprto tijelom ExternName koje završava dvobajtnim $1C $17 PtgErr koji drži #REF!
XLSX čuva sirovi tekst formule, a ODS svoju msoxl: formulu, pa se datoteka koja je spremila =WEBSERVICE(...) otvara s tekstom netaknutim i i dalje evaluira u xlfeUnsafeFunctionDenied po defaultu

Ako Vaš pipeline evaluira radne knjige koje nije sam autor, ostavite AllowUnsafeFormulaCallbacks na False, držite handlere na eksplicitnom allowlistu i dodijelite opcije po pozivu samo tamo gdje je izvor formule Vaš. Cijeli callback, entry-policy i evaluacijski API dokumentiran je uz HotXLS Delphi spreadsheet komponentu