Techninis straipsnis

HotXLS nesaugios formulių callback: CALL ir WEBSERVICE

HotXLS atsisako nukreipti į jūsų Delphi vartotojo funkcijų callback 20 pavojingų formulių vardų, įskaitant CALL, REGISTER.ID, WEBSERVICE ir DDE, nebent to pats pageisite. Darbaknygės savybė AllowUnsafeFormulaCallbacks pagal nutylėjimą False, patikra paleidžiama dar prieš vertinant bent vieną argumentą, o atmestas kvietimas praneša xlfeUnsafeFunctionDenied nepaleidęs nė vieno handlerio

Scenarijus, dėl kurio to prireikė, visai kasdieniškas. Servisas priima įkeltus XLS arba XLSX failus, perskaičiuoja juos serveryje ir perskaito kelias sumas atgal. Šeiminė programa prieš kelerius metus užregistravo OnUserFunction handlerį kelioms verslo funkcijoms, ir kažkur pakelyje tas handleris užaugo catch-all šaka, kuri viską, ko neatpažįsta, persiunčia į papildinių lentelę. Nė vienas komandos narys niekada nėra įvedęs =WEBSERVICE(...) į ląstelę. Įkeliantysis įvedė. Išlaikyti tą formulę nepaliestą per atidarymą, perskaičiavimą ir įrašymą yra failo ištikimybės funkcija. Leisti jai pasiekti šeiminį kodą, galintį atidaryti soketus ar failus, yra autorizacijos sprendimas, ir kol HotXLS abu dalykus atskyrė, biblioteka tą sprendimą tyliai priimdavo už jus

Kodėl formulės išsaugojimas virto leidimu ją paleisti?

Šakninė priežastis buvo vienas atsarginis kelias. HotXLS išanalizuoja kiekvieną jam žinomą Excel funkcijos vardą, bet ne kiekvienas žinomas vardas turi implementaciją skaičiavimo variklyje. Įtaisytieji, atpažįstami, bet neįgyvendinti vardai anksčiau nukrisdavo į tą patį vartotojo funkcijų atsarginį kelią kaip tikrai savos vardos, tad CALL ir REGISTER.ID dalindavosi dispečio keliu su jūsų DISCOUNT ar REGIONRATE. Nežinomi vardai, tokie kaip WEBSERVICE ar DDE, panašiai galėjo sutapti su to paties vardo įrašu darbaknygės registre, viso proceso registre arba įvykių handler'yje. To atsarginio kelio mechanika aprašyta kaip HotXLS išsprendžia savas funkcijas per OnUserFunction; problema buvo tai, kad niekas tame kelyje nepaklausdavo, ar pats vardas yra toks, kurį sveikas hostas apskritai turėtų leisti

Dispečio tvarka svarbi tam, ką čia reiškia „nežinomas“. Kvietimas, kurio variklis negali įvertinti savomis jėgomis, paeiliui siūlomas leksinėms LAMBDA ir LET priklausomybėms, kurias pirmiausiai išsprendžia uždaročių palaikymas HotXLS formulių variklyje, tada darbaknygės lokaliosioms funkcijoms, užregistruotoms per RegisterUserFunction, tada viso proceso funkcijoms iš TXLSWorkbook.RegisterGlobalUserFunction ir galiausiai OnUserFunction ir OnUserFunctionEx įvykiams. Tik kai visi jie atsisako, tikrai nežinoma funkcija tampa #NAME?. Kiekvienas etapas po lambda paieškos perduoda valdymą jūsų pačių kodui — būtent todėl saugos patikra turi stovėti prieš visą grandinę, o ne kurio nors vieno handlerio viduje

Kaip HotXLS užtveria nesaugius formulių callback: GetValueItemUserFunction patikrina vardą dar prieš sukuriant argumentų masyvą ar paleidžiant bet kokį valdytoją, tad įdėta =WEBSERVICE(AUDIT_TOKEN()) išeina su lxErrorUnsafeFunctionDenied, o audito žurnalas lieka tuščias, kol saugūs kvietimai eina grandine nuo LAMBDA ir LET priklausomybių žemyn iki OnUserFunction
Tik kai visi etapai atsisako, tikrai nežinoma funkcija tampa #NAME?, todėl saugos patikra stovi prieš visą grandinę, o ne kurio nors vieno handlerio viduje

Kuriuos funkcijų vardus HotXLS blokuoja pagal nutylėjimą?

XLSFormulaCallbackIsUnsafe faile lxCalc.pas laiko fiksuotą 20 vardų atmetimo aibę: 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 ir FILE.DELETE. Tai vardai, kurie Excel'yje ar jo makrokalboje įkelia savąjį kodą, pasiekia tinklą, kalbasi su kitais procesais ar paliečia failų sistemą. Prieš palyginimą funkcija nukerpa aplinkinius tarpus, vardą padaro didžiosiomis raidėmis ir nulupa vieną _XLFN. arba _XLWS. priešdėlį, tad naujesnės Excel versijos parašytą _xlfn.webservice sugauna taip pat, kaip ir pliką rašybą. Sąrašas gyvena skaičiuoklės riboje, o ne Classic, XLSX ir ODS analizatoriuose, todėl vienas AST, vienas BIFF tokenų srautas ir viena konvertuota darbaknygė elgiasi identiškai

Kaip HotXLS suvienodina funkcijos vardą prieš nesaugių callback palyginimą: tarpai nukerpami, vardas padaromas didžiosiomis raidėmis ir nulupamas vienas _XLFN. arba _XLWS. priešdėlis, tad _xlfn.webservice sugaunamas kaip plika rašyba, tada rezultatas tiksliai sugretinamas su fiksuota 20 vardų atmetimo aibe lxCalc.pas
Sąrašas dengia vardus, kurie įkelia savąjį kodą, pasiekia tinklą, kalbasi su kitais procesais ar paliečia failų sistemą — nuo DDE, CALL ir WEBSERVICE iki FWRITE ir FILE.DELETE

Dvi smulkmenos vertos žinoti, prieš jumis pasitikint tuo. Sutapimas tikslus, tad jūsų užregistruotas MYWEBSERVICE handlerio nepaliečia, ir atvirkščiai — teisėta vidinė UDF, atsitiktinai vadinama OPEN ar RUN, dabar pagal nutylėjimą atmetama. Atmetimo aibė taip pat nėra jūsų pačių handlerių smėlio dėžė. Jei jūsų catch-all šaka vykdo savavališkus papildinių vardus, vartai sustabdo garsiuosius pavojinguosius ir nieko daugiau; patvari pataisa vis dar yra handleris, kuris SameText lygina su aiškiu leidžiamųjų vardų sąrašu ir viskam, ko nepriklauso, palieka Handled False

Kodėl vartai turi veikti prieš argumentų vertinimą?

Vartai, užsidegantys po to, kai argumentai jau apskaičiuoti, pavėluoti, nes patys argumentai gali iškviesti jūsų kodą. GetValueItemUserFunction vardą patikrina pirmiausia ir išeina su lxErrorUnsafeFunctionDenied dar nesukūręs argumentų masyvo, dar nepasiklausęs nė valdytojo, nė kurio registro, ir net dar nepastebėjęs, kad iš viso nepriskirtas joks handleris. Ta tvarka ir nugalimas žemiau esantis įdėtasis atvejis, kai išorinis kvietimas būtų atsisakytas bet kaip, bet nepastebimai atrodanti vidinė UDF kitu atveju suveiktų pirma ir paliktų savo šalutinį poveikį

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');   // šalutinis poveikis šeiminiame kode
    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, o FAuditLog vis dar tuščias

Darbaknygės numatytoji reikšmė prieš per kvietimą skirtos TXLSFormulaEvaluationOptions

Darbaknygės žymė yra numatytoji, o per kvietimą skirta parinktis — paskutinis žodis. TXLSWorkbook.AllowUnsafeFormulaCallbacks ir TXLSXWorkbook.AllowUnsafeFormulaCallbacks valdo įprastą perskaičiavimą, Calculate, dviejų argumentų EvaluateFormulaAt, vertinimo šablonus, tik skaitomus rodinius ir, XLSX atveju, kiekvieną darbuotoją lygiagretaus perskaičiavimo base. Bet koks įėjimo taškas, priimantis aiškų TXLSFormulaEvaluationOptions įrašą, tam kvietimui paskelbia verdiktą Options.AllowUnsafeFormulaCallbacks ir jo nesumaišo su darbaknygės savybe OR operacija. Ta asimetrija sąmoninga: patikėta vidinė užduotis gali suteikti vienai RTD paieškai leidimą neversdama visos darbaknygės, o globaliai įjungta darbaknygė vis tiek gali jautrų vertinimą grąžinti prie atmetimo

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // darbaknygė lieka užrakinta, vienas patikėtas kvietimas praleidžiamas
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // darbaknygė įjungta, bet šis įkeltos tekstyno vertinimas — ne
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // žymė vėl False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Darbaknygės savybės perjungimas abiejuose varikliuose ir priklausomybių grafą pažymi nešvariu. Be to žingsnio podėlyje sukaupta reikšmė, apskaičiuota, kol callback buvo leisti, galėtų būti patiekiama juos atšaukus, arba podėlyje sukautas xlfeUnsafeFunctionDenied rezultatas galėtų išgyventi įjungimą. Naujasis statusas prie TXLSFormulaEvaluationStatus pridėtas po xlfeFailed, tad jo ordinalinis numeris 10 ir kiekviena esama reikšmė išlaiko savo vertę; ta pati galo pridėjimo taisyklė galioja parinkčių įrašo laukui ir IXLSWorkbook getteriui bei setteriui, nors vartotojas, sukurtas prie senesnės laidos, vis tiek reikalauja perkompiliavimo

Kas nutinka nežinomam ir nesaugiam formulės tekstui įrašant?

Formulės išlaikymas ir jos paleidimas dabar du atskiri klausimai, o įvedimo politika atsako tik į pirmąjį. FormulaEntryPolicy abiejose darbaknygių klasėse neša UnknownFunctionMode ir UnknownNameMode, abu pagal nutylėjimą xlfusmReject, tad formulės su nežinomu kvietimu priskyrimas per įprastą Formula savybę atmetamas, dar nepakeitus nei ląstelės reikšmės, nei formulių podėlio, nei priklausomybių. ValidateFormulaEntry tą patį sprendimą praneša be šalutinio poveikio. Patikėti keliai, tokie kaip failo įkėlimas, kopijavimas ir formato konversija, aplenkia tą vartotojo įvedimo politiką, nes griežtas numatytasis niekada neturi atmesti simbolių, jau esančių faile, kurį jūs tiesiog atidarote

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // suderinamumo įvedimas
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // išsaugota, ne autorizuota
  Book.SaveAs('rates.xls');
end;

Klasikiniame BIFF8 nežinomas kvietimas neturi savo tokeno, tad HotXLS jį rašo taip, kaip Excel rašo papildinių funkcijas. Formulė gauna PtgNameX tokeną ($59), kurio XTI įrašas rodo į papildinio SUPBOOK su abiem lapų indeksais, nustatytais į $FFFE, po jo eina argumentų tokenai ir PtgFuncVar su funkcijos numeriu 255 ir argumentų skaičiumi, įskaitančiu vardo vietą. Už jo stovintis ExternName kūnas yra šeši nuliniai baitai, ilgio baitas ir Unicode žymė, UTF-16 funkcijos vardas, tada dviejų baitų formulė $1C $17 — PtgErr, laikantis #REF!. Rašytojas atsisako vardų ilgesnių nei 255 simboliai, daugiau nei 29 argumentų ir BIFF5 tikslo. Kaip HotXLS šiuos papildinių SUPBOOK įrašus klasifikuoja šalia išorinių darbaknygių nuorodų, aiškinta SUPBOOK ir XTI klasifikavimo taisyklėse BIFF išorinėms nuorodoms. XLSX išlaiko žalią funkcijos tekstą, ODS — savą msoxl: formulę, ir kiekviename formate failas, išsaugojęs =WEBSERVICE(...), vėl atsidaro su tekstu nepaliestu ir pagal nutylėjimą vis tiek vertinamas į xlfeUnsafeFunctionDenied

Kaip HotXLS nežinomą formulės kvietimą įrašo į klasikinį BIFF8: formulė neša PtgNameX tokeną, kurio XTI įrašas rodo į papildinio SUPBOOK su abiem lapų indeksais $FFFE, tada argumentų tokenus ir PtgFuncVar su funkcijos numeriu 255, remiamą ExternName kūno, besibaigiančio dviejų baitų $1C $17 PtgErr, laikančiu #REF!
XLSX išlaiko žalią funkcijos tekstą, ODS — savą msoxl: formulę, tad failas, išsaugojęs =WEBSERVICE(...), vėl atsidaro su tekstu nepaliestu ir pagal nutylėjimą vis tiek vertinamas į xlfeUnsafeFunctionDenied

Jei jūsų srautas vertina darbaknyges, kurių pats nesudėliojo, palikite AllowUnsafeFormulaCallbacks False, laikykite handlerius aiškiame leidžiamųjų vardų sąraše, o per kvietimą skirtas parinktis suteikite tik ten, kur formulės šaltinis jūsų. Pilnas callback, įvedimo politikos ir vertinimo API dokumentuotas prie HotXLS Delphi spreadsheet komponento