Articol tehnic

Formula callbacks nesigure în HotXLS: CALL și WEBSERVICE

HotXLS refuză să routeze 20 de nume de formule periculoase, printre care CALL, REGISTER.ID, WEBSERVICE și DDE, către callback-urile dvs. de funcții utilizator, dacă nu optați explicit. Proprietatea registrului de lucru AllowUnsafeFormulaCallbacks are implicit valoarea False, verificarea rulează înainte să fie evaluat orice argument, iar un apel respins raportează xlfeUnsafeFunctionDenied fără să invoce niciun handler

Scenariul care a făcut asta necesară este banal. Un serviciu acceptă fișiere XLS sau XLSX încărcate, le recalculează pe server și citește înapoi câteva totale. Aplicația gazdă înregistrase acum ani un handler OnUserFunction pentru câteva funcții de business, iar pe undeva pe parcurs acel handler căpătase o ramură catch-all care trimite orice nu recunoaște către un tabel de plugin-uri. Nimeni din echipă nu a tastat niciodată =WEBSERVICE(...) într-o celulă. Cel care încarcă a făcut-o. Să păstrezi formula aia intactă prin deschidere, recalcul și salvare este o funcție de fidelitate a fișierului. Să o lași să ajungă în cod gazdă care poate deschide socket-uri sau fișiere este o decizie de autorizare, și până când HotXLS a separat cele două, biblioteca lua în tăcere decizia aceea în locul dumneavoastră

De ce a ajuns păstrarea unei formule să însemne permisiunea de a o rula?

Cauza rădăcină a fost o singură cale de fallback. HotXLS parsează fiecare nume de funcție Excel pe care îl cunoaște, dar nu fiecare nume cunoscut are o implementare în motorul de calcul. Built-in-urile recunoscute dar neimplementate cădeau înainte în același fallback de funcții definite de utilizator ca numele cu adevărat custom, astfel încât CALL și REGISTER.ID împărtșeau o cale de dispatch cu DISCOUNT sau REGIONRATE ale dvs. Numele necunoscute precum WEBSERVICE sau DDE se puteau de asemenea potrivi cu o intrare cu același nume în registrul registrului de lucru, în registrul la nivel de proces sau într-un handler de eveniment. Mecanica acelui fallback este acoperită în cum rezolvă HotXLS funcțiile custom prin OnUserFunction; problema a fost că nimic de pe calea aceea nu întreba dacă numele în sine este unul pe care o gazdă sănătoasă ar trebui vreodată să îl execute

Ordinea de dispatch contează pentru ce înseamnă „necunoscut" aici. Un apel pe care motorul nu îl poate evalua nativ este oferit pe rând legăturilor lexicale LAMBDA și LET, pe care suportul de closure-uri din motorul de formule HotXLS le rezolvă primele, apoi funcțiilor locale registrului de lucru înregistrate cu RegisterUserFunction, apoi funcțiilor la nivel de proces din TXLSWorkbook.RegisterGlobalUserFunction, și în final evenimentelor OnUserFunction și OnUserFunctionEx. Doar când toate refuză, o funcție cu adevărat necunoscută devine #NAME?. Fiecare etapă de după căutarea lambda predă controlul codului scris de dvs., ceea ce este exact motivul pentru care verificarea de siguranță trebuie să steie în fața întregului lanț, nu în interiorul vreunui handler

Cum filtrează HotXLS callback-urile de formule nesigure: GetValueItemUserFunction verifică numele înainte ca tabloul de argumente să fie construit sau vreun rezolvitor să ruleze, astfel încât un =WEBSERVICE(AUDIT_TOKEN()) imbricat iese cu lxErrorUnsafeFunctionDenied și jurnalul de audit rămâne gol, în timp ce apelurile sigure parcurg lanțul de la legăturile LAMBDA și LET până la OnUserFunction
Doar când fiecare etapă refuză, o funcție cu adevărat necunoscută devine #NAME?, motiv pentru care verificarea de siguranță stă în fața întregului lanț, nu în interiorul vreunui handler

Ce nume de funcții blochează HotXLS implicit?

XLSFormulaCallbackIsUnsafe din lxCalc.pas ține o mulțime fixă de respingere cu 20 de nume: 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. Ele sunt numele care, în Excel sau în limbajul lui de macro-uri, încarcă cod nativ, ating rețeaua, vorbesc cu alte procese sau ating sistemul de fișiere. Înainte de comparație, funcția trimează spațiile albe din jur, trece numele cu majuscule și îndepărtează un singur prefix _XLFN. sau _XLWS., astfel încât _xlfn.webservice scris de o construcție Excel mai nouă este prins la fel ca ortografia goală. Lista trăiește la granița calculatorului, nu în parserele Classic, XLSX și ODS, ceea ce ține un singur AST, un singur flux de token-uri BIFF și un singur registru de lucru convertit comportându-se identic

Cum normalizează HotXLS un nume de funcție înainte de comparația de callback nesigur: spațiile albe sunt trimate, numele este trecut cu majuscule și un singur prefix _XLFN. sau _XLWS. este îndepărtat, astfel încât _xlfn.webservice este prins ca ortografia goală, apoi rezultatul este potrivit exact cu mulțimea fixă de respingere cu 20 de nume din lxCalc.pas
Lista acoperă numele care încarcă cod nativ, ating rețeaua, vorbesc cu alte procese sau ating sistemul de fișiere, de la DDE, CALL și WEBSERVICE până la FWRITE și FILE.DELETE

Două margini merită știute înainte să vă bazați pe ea. Potrivirea este exactă, deci un handler pe care îl înregistrați ca MYWEBSERVICE nu este afectat, și invers, un UDF legitim intern care se numește din întâmplare OPEN sau RUN este acum respins implicit. Mulțimea de respingere nu este niciun sandbox pentru handler-urile proprii. Dacă ramura dvs. catch-all execută nume de plugin-uri oarecare, poarta oprește celebrele periculoase și nimic altceva; reparația durabilă rămâne un handler care potrivește o listă explicită de permise cu SameText și lasă Handled pe False pentru tot ce nu îi aparține

De ce trebuie poarta să ruleze înainte de evaluarea argumentelor?

O poartă care se declanșează după ce argumentele au fost calculate vine prea târziu, pentru că argumentele în sine vă pot apela codul. GetValueItemUserFunction verifică numele întâi și iese cu lxErrorUnsafeFunctionDenied înainte să construiască tabloul de argumente, înainte să consulte un rezolvitor sau oricare dintre registre, și chiar înainte să observe că nu este atribuit niciun handler. Acea ordine este ceea ce învinge cazul imbricat de mai jos, în care apelul exterior ar fi respins oricum, dar un UDF interior cu aspect inofensiv s-ar declanșa altfel primul și și-ar lăsa efectul secundar în urmă

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');   // efect secundar în cod gazdă
    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, iar FAuditLog este încă gol

Implicit la registru versus TXLSFormulaEvaluationOptions per apel

Flag-ul registrului de lucru este implicit, iar opțiunea per apel are ultimul cuvânt. TXLSWorkbook.AllowUnsafeFormulaCallbacks și TXLSXWorkbook.AllowUnsafeFormulaCallbacks guvernează recalcularea obișnuită, Calculate, EvaluateFormulaAt cu două argumente, șabloanele de evaluare, vizualizările doar în citire și, pe XLSX, fiecare worker din bazinul de recalculare paralel. Orice punct de intrare care acceptă un record explicit TXLSFormulaEvaluationOptions ia Options.AllowUnsafeFormulaCallbacks ca verdict pentru acel apel și nu îl OR-ează cu proprietatea registrului de lucru. Asimetria aceea este deliberată: un job intern de încredere poate autoriza o singură interogare RTD fără să răstoarne întregul registru de lucru, iar un registru de lucru activat global poate totuși forța o evaluare sensibilă înapoi la respingere

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // registrul de lucru rămâne închis, un singur apel de încredere primește trecere
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // registrul de lucru activat, dar această evaluare a textului încărcat nu
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // flag-ul este din nou False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

Comutarea proprietății registrului de lucru marchează de asemenea graful de dependențe ca murdar pe ambele motoare. Fără pasul acela, un rezultat cachetat calculat cât timp callback-urile erau permise ar putea fi servit după ce au fost revocate, sau un rezultat cachetat xlfeUnsafeFunctionDenied ar putea supraviețui unei activări. Noul status a fost adăugat la coada lui TXLSFormulaEvaluationStatus după xlfeFailed, deci are ordinalul 10 și fiecare ordinal existent își păstrează valoarea; aceeași regulă de adăugare la coadă se aplică câmpului din recordul de opțiuni și getter-ului și setter-ului IXLSWorkbook, deși un consumator construit contra unei versiuni mai vechi are totuși nevoie de recompilare

Ce se întâmplă cu textul formulelor necunoscute și nesigure la salvare?

Să păstrezi o formulă și să o rulezi sunt acum două întrebări separate, iar politica de intrare răspunde doar la prima. FormulaEntryPolicy pe oricare clasă de registru de lucru poartă UnknownFunctionMode și UnknownNameMode, ambele cu implicit xlfusmReject, astfel încât atribuirea unei formule cu un apel necunoscut prin proprietatea obișnuită Formula este respinsă înainte să se schimbe valoarea celulei, cache-ul de formule sau dependențele. ValidateFormulaEntry raportează aceeași decizie fără efecte secundare. Căile de încredere precum încărcarea fișierelor, copierea și conversia de format ocolesc politica aceea de intrare a utilizatorului, pentru că un implicit strict nu trebuie să respingă niciodată simboluri deja prezente într-un fișier pe care pur și simplu îl deschideți

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // intrare de compatibilitate
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // stocată, nu autorizată
  Book.SaveAs('rates.xls');
end;

În BIFF8 clasic, un apel necunoscut nu are un token al lui, deci HotXLS îl scrie așa cum scrie Excel funcțiile de add-in. Formula primește un token PtgNameX ($59) a cărui intrare XTI arată către SUPBOOK-ul de add-in cu ambele indexuri de foaie setate la $FFFE, urmată de token-urile de argumente și un PtgFuncVar care poartă numărul de funcție 255 și un număr de argumente care include slotul de nume. Corpul ExternName de rezervă este șase byte de zero, un byte de lungime și flag-ul Unicode, numele funcției UTF-16, apoi o formulă de doi byte $1C $17, un PtgErr care ține #REF!. Scriitorul refuză numele mai lungi de 255 de caractere, mai mult de 29 de argumente și ținta BIFF5. Felul în care HotXLS clasifică aceste intrări SUPBOOK de add-in lângă legăturile către registre de lucru externe este explicat în regulile de clasificare SUPBOOK și XTI pentru legăturile externe BIFF. XLSX păstrează textul brut al funcției, iar ODS își păstrează formula msoxl:, iar în orice format un fișier care a salvat =WEBSERVICE(...) se redeschide cu textul intact și se evaluează tot la xlfeUnsafeFunctionDenied implicit

Cum scrie HotXLS un apel de formulă necunoscut în BIFF8 clasic: formula poartă un token PtgNameX a cărui intrare XTI arată către SUPBOOK-ul de add-in cu ambele indexuri de foaie $FFFE, apoi token-urile de argumente și un PtgFuncVar cu numărul de funcție 255, susținut de un corp ExternName care se termină cu un PtgErr de doi byte $1C $17 ținând #REF!
XLSX păstrează textul brut al funcției, iar ODS își păstrează formula msoxl:, deci un fișier care a salvat =WEBSERVICE(...) se redeschide cu textul intact și se evaluează tot la xlfeUnsafeFunctionDenied implicit

Dacă pipeline-ul dvs. evaluează registre de lucru pe care nu le-a scris el, lăsați AllowUnsafeFormulaCallbacks pe False, țineți handler-urile pe o listă explicită de permise și acordați opțiuni per apel doar acolo unde sursa formulei este a dvs. API-ul complet de callback, politică de intrare și evaluare este documentat cu componenta de foi de calcul HotXLS pentru Delphi