Tekninen artikkeli

HotXLS estää CALL- ja WEBSERVICE-kaavacallbackit oletuksena

HotXLS kieltäytyy reitittämästä 20 vaarallista kaavanimeä, mukaan lukien CALL, REGISTER.ID, WEBSERVICE ja DDE, Delphin käyttäjäfunktiocallbackeihisi ellet valitse toiminnon käyttöön. Työkirjan ominaisuuden AllowUnsafeFormulaCallbacks oletus on False, tarkistus ajetaan ennen kuin yhtään argumenttia evaluoidaan, ja hylätty kutsu raportoi xlfeUnsafeFunctionDeniedin kutsumatta yhtään käsittelijää

Skenaario joka teki tästä tarpeelliseksi on arkinen. Palvelu ottaa vastaan ladattuja XLS- tai XLSX-tiedostoja, laskee ne uudelleen palvelinpuolella ja lukee pari summaa takaisin. Isäntäsovellus rekisteröi OnUserFunction-käsittelijän vuosia sitten paria liiketoimintafunktiota varten, ja jossakin matkan varrella käsittelijä kasvatti haarautuman joka napkaa kaiken tunnistamattoman ja välittää sen liitännäistaulukkoon. Kukaan tiimiläisistä ei koskaan näpyttänyt =WEBSERVICE(...)ia soluun. Lataaja teki. Sen kaavan pitäminen ennallaan avauksen, uudelleenlaskennan ja tallennuksen läpi on tiedoston uskollisuusominaisuus. Sen päästäminen isäntäkoodiin joka voi avata socketteja tai tiedostoja on valtuutuspäätös, ja ennen kuin HotXLS erotti nuo kaksi, kirjasto teki sen päätöksen hiljaa puolestasi

Miksi kaavan säilyttäminen muuttui luvaksi ajaa se?

Juurisyy oli yksi varapolku. HotXLS jäsentää jokaisen tuntemansa Excel-funktionimen, mutta kaikki tunnetut nimet eivät ole toteutettuja laskentamoottorissa. Tunnistetut mutta toteuttamattomat valmiit funktiot putosivat aiemmin samaan käyttäjän määrittelemien funktioiden varapolkuun kuin aidosti omat nimet, joten CALL ja REGISTER.ID jakoivat dispatch-polun DISCOUNTisi tai REGIONRATEisi kanssa. Tuntemattomat nimet kuten WEBSERVICE tai DDE saattoivat vastaavasti osua samannimiseen merkintään työkirjarekisterissä, prosessinlaajuisessa rekisterissä tai tapahtumakäsittelijässä. Kyseisen varapolun mekaniikat käsitellään artikkelissa miten HotXLS ratkaisee omat funktiot OnUserFunctionin kautta; ongelma oli että mikään polulla ei kysynyt onko nimi itsessään sellainen jonka järkevä isäntä koskaan ajaisi

Dispatch-järjestyksellä on väliä sille mitä ”tuntematon” tässä tarkoittaa. Kutsua jota moottori ei osaa evaluoida natiivisti tarjotaan vuorollaan leksikaalisille LAMBDA- ja LET-sidonnoille, jotka HotXLS:n kaavamoottorin closure-tuki ratkaisee ensin, sitten RegisterUserFunctionilla rekisteröidyille työkirjakohtaisille funktioille, sitten prosessinlaajuisille funktioille TXLSWorkbook.RegisterGlobalUserFunctionista ja viimeksi OnUserFunction- ja OnUserFunctionEx-tapahtumille. Vasta kun kaikki kieltäytyvät, aidosti tuntematon funktio muuttuu arvoksi #NAME?. Jokainen vaihe lambda-haun jälkeen luovuttaa ohjauksen kirjoittamallesi koodille, mikä on juuri syy sille että turvatarkistuksen on istuttava koko ketjun edessä eikä minkään yhden käsittelijän sisällä

Miten HotXLS portittaa turvattomat kaavacallbackit: GetValueItemUserFunction tarkistaa nimen ennen kuin argumentti-aulokko rakennetaan tai yksikään resolveri ajetaan, joten sisäkkäinen =WEBSERVICE(AUDIT_TOKEN()) poistuu arvolla lxErrorUnsafeFunctionDenied ja auditointiloki pysyy tyhjänä, kun taas turvalliset kutsut kulkevat ketjun LAMBDA- ja LET-sidonnaisista OnUserFunctioniin
Vasta kun jokainen vaihe kieltäytyy, aidosti tuntematon funktio muuttuu arvoksi #NAME?, mikä on syy sille että turvatarkistus istuu koko ketjun edessä eikä minkään yhden käsittelijän sisällä

Mitkä funktioiden nimet HotXLS estää oletuksena?

XLSFormulaCallbackIsUnsafe tiedostossa lxCalc.pas pitää kiinteää 20 nimen estolistaa: 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 ja FILE.DELETE. Ne ovat nimet jotka Excelissä tai sen makrokielessä lataavat natiivikoodia, ulottuvat verkkoon, puhuvat muille prosesseille tai koskettavat tiedostojärjestelmää. Ennen vertailua funktio trimmaa ympäröivän tyhjän, muuntaa nimen isoiksi kirjaimiksi ja irrottaa yhden _XLFN.- tai _XLWS.-etuliitteen, joten uudemman Excel-version kirjoittama _xlfn.webservice jää kiinni aivan kuten paljas kirjoitusasu. Lista asuu laskurin rajapinnassa eikä Classic-, XLSX- ja ODS-jäsentimissä, mikä pitää yhden AST:n, yhden BIFF-tokenvirran ja yhden konvertoidun työkirjan käyttäytymässä identtisesti

Miten HotXLS normalisoi funktion nimen ennen turvattoman callbackin vertailua: tyhjä trimmataan, nimi muunnetaan isoiksi kirjaimiksi ja yksi _XLFN.- tai _XLWS.-etuliite irrotetaan joten _xlfn.webservice jää kiinni kuten paljas kirjoitusasu, sitten tulos vertaillaan täsmälleen lxCalc.pasin kiinteään 20 nimen estolistaan
Lista kattaa natiivia koodia lataavat, verkkoon ulottuvat, muihin prosesseihin puhuvat ja tiedostojärjestelmää koskettavat nimet DDE:stä ja CALL:sta FWRITEen ja FILE.DELETEen

Kaksi reunaehdoa kannattaa tuntea ennen kuin luotat siihen. Vertailu on täsmällinen, joten MYWEBSERVICEiksi rekisteröimäsi käsittelijä ei vaikudu, ja toisaalta laillinen in-house UDF joka sattuu nimiin OPEN tai RUN hylätään nyt oletuksena. Estolista ei myöskään ole hiekkalaatikko omille käsittelijöillesi. Jos kaiken napkaiseva haarautumasi ajaa mielivaltaisia liitännäisnimiä, portti pysäyttää kuuluisat vaaralliset eikä muuta; kestävä korjaus on yhä käsittelijä joka vertaa eksplisiittiseen sallittujen listaan SameTextilla ja jättää Handledin arvoon False kaikelle jota se ei omista

Miksi portin on ajettava ennen argumenttien evaluointia?

Portti joka laukeaa vasta kun argumentit on laskettu on myöhässä, koska argumentit itsessään voivat kutsua koodiasi. GetValueItemUserFunction tarkistaa nimen ensin ja poistuu arvolla lxErrorUnsafeFunctionDenied ennen kuin se rakentaa argumentti-aulokon, ennen kuin se konsultoi resolveria tai kumpaakin rekisteriä, ja jopa ennen kuin se huomaa ettei yhtään käsittelijää ole sijoitettu. Se järjestys on se mikä kaataa alla olevan sisäkkäisen tapauksen, jossa ulompi kutsu hylättäisiin joka tapauksessa mutta harmiton näköinen sisempi UDF muuten lauhtuisi ensin ja jättäisi sivuvaikutuksensa jälkeensä

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');   // sivuvaikutus isäntäkoodissa
    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, ja FAuditLog on yhä tyhjä

Työkirjan oletus vastaan kutsukohtainen TXLSFormulaEvaluationOptions

Työkirjan lippu on oletus ja kutsukohtainen asetus on viimeinen sana. TXLSWorkbook.AllowUnsafeFormulaCallbacks ja TXLSXWorkbook.AllowUnsafeFormulaCallbacks hallitsevat tavallista uudelleenlaskentaa, Calculatea, kaksiparametrista EvaluateFormulaAtia, evaluointipohjia, vain luku -näkymiä ja XLSX:n osalta jokaista työntekijää rinnakkaisessa uudelleenlaskentapoolissa. Jokainen sisääntulopiste joka ottaa vastaan eksplisiittisen TXLSFormulaEvaluationOptions-recordin ottaa Options.AllowUnsafeFormulaCallbacksin tuomiona kyseiselle kutsulle eikä tee sille OR-operaatiota työkirjan ominaisuuden kanssa. Se epäsymmetria on tarkoituksellinen: luotettu sisäinen eräajo voi valtuuttaa yhden RTD-haun kääntämättä koko työkirjaa, ja globaalisti valittu työkirja voi silti pakottaa arkaluontoisen evaluoinnin takaisin hylkäykseen

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // työkirja pysyy lukittuna, yksi luotettu kutsu pääsee läpi
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // työkirja valittu käyttöön, mutta tämä ladatun tekstin evaluointi ei ole
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // lippu on taas False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Työkirjan ominaisuuden vaihtaminen merkitsee myös riippuvuusgraafin likaiseksi molemmissa koneissa. Ilman sitä välimuistissa oleva tulos joka laskettiin kun callbackit olivat sallittuja voitaisiin palvella sen jälkeen kun ne oli peruttu, tai välimuistissa oleva xlfeUnsafeFunctionDenied-lopputulos voi elää opt-inin yli. Uusi tila liitettiin TXLSFormulaEvaluationStatusin xlfeFailedin perään, joten sen ordinaali on 10 ja jokainen olemassa oleva ordinaali säilyttää arvonsa; sama hännälle liittämissääntö pätee options-recordin kenttään ja IXLSWorkbook-getteriin ja -setteriin, vaikka vanhempaa julkaisua vasten rakennettu kuluttaja tarvitsee yhä uudelleenkäännöksen

Mitä tuntemattomalle ja turvattomalle kaavatekstille tapahtuu tallennuksessa?

Kaavan säilyttäminen ja sen ajaminen ovat nyt kaksi eri kysymystä, ja sisääntulokäytäntö vastaa vain ensimmäiseen. FormulaEntryPolicy kummassakin työkirjaluokassa kantaa UnknownFunctionModen ja UnknownNameModen, molemmat oletuksena xlfusmReject, joten tuntemattoman kutsun sisältävän kaavan sijoittaminen tavallisen Formula-ominaisuuden kautta hylätään ennen kuin solun arvo, kaavavälimuisti tai riippuvuudet muuttuvat. ValidateFormulaEntry raportoi saman päätöksen ilman sivuvaikutuksia. Luotetut polut kuten tiedoston lataaminen, kopioiminen ja muotojen konversio ohittavat kyseisen käyttäjän sisääntulokäytännön, koska tiukka oletus ei saa koskaan hylätä symboleita jotka ovat jo tiedostossa jota olet vain avaamassa

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // yhteensopivuusmerkintä
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // tallennettu, ei valtuutettu
  Book.SaveAs('rates.xls');
end;

Klassisessa BIFF8:ssa tuntemattomalla kutsulla ei ole omaa tokenia, joten HotXLS kirjoittaa sen sillä tavalla kuin Excel kirjoittaa add-in-funktiot. Kaava saa PtgNameX-tokenin ($59) jonka XTI-merkintä osoittaa add-in-SUPBOOKiin molempien sheet-indeksien ollessa $FFFE, jota seuraavat argumenttitokenit ja PtgFuncVar joka kantaa funktionumeroa 255 ja argumenttien määrää joka sisältää nimipaikan. Taustalla oleva ExternName-runko on kuusi nollatavua, pituustavu ja Unicode-lippu, UTF-16-funktionimi, sitten kaksitavuinen kaava $1C $17, PtgErr joka pitää sisällään arvon #REF!. Kirjoittaja kieltäytyy yli 255 merkin nimistä, yli 29 argumentista ja BIFF5-kohteesta. Miten HotXLS luokittelee nämä add-in-SUPBOOK-merkinnät ulkoisten työkirjalinkkien rinnalla selittää SUPBOOK- ja XTI-luokittelusäännöt BIFF-ulkoisille linkeille. XLSX pitää raakan funktion tekstin ja ODS pitää msoxl:-kaavansa, ja joka formaatissa tiedosto joka tallensi =WEBSERVICE(...)in avautuu uudelleen tekstin ollessa ennallaan ja evaluoituu yhä oletuksena arvoon xlfeUnsafeFunctionDenied

Miten HotXLS kirjoittaa tuntemattoman kaavakutsun klassiseen BIFF8:aan: kaava kantaa PtgNameX-tokenia jonka XTI-merkintä osoittaa add-in-SUPBOOKiin molempien sheet-indeksien ollessa $FFFE, sitten argumenttitokenit ja PtgFuncVar funktionumerolla 255, taustalla ExternName-runko joka päättyy kaksitavuiseen $1C $17 PtgErriin joka pitää sisällään #REF!:n
XLSX pitää raakan funktion tekstin ja ODS pitää msoxl:-kaavansa, joten tiedosto joka tallensi =WEBSERVICE(...)-kutsun avautuu uudelleen tekstin ollessa ennallaan ja evaluoituu yhä oletuksena arvoon xlfeUnsafeFunctionDenied

Jos putkesi evaluoi työkirjoja joita se ei itse kirjoittanut, jätä AllowUnsafeFormulaCallbacks arvoon False, pidä käsittelijät eksplisiittisellä sallittujen listalla ja myönnä kutsukohtaiset asetukset vain siellä missä kaavalähde on sinun. Koko callback-, sisääntulokäytäntö- ja evaluointi-API on dokumentoitu HotXLS Delphi spreadsheet component -komponentin yhteydessä