مقاله فنی

callbackهای فرمول ناامن HotXLS: کنترل CALL و WEBSERVICE

HotXLS مگر اینکه خودتان opt-in کنید، 20 نام فرمول خطرناک از جمله CALL و REGISTER.ID و WEBSERVICE و DDE را به callbackهای user-function دلفی‌تان مسیر نمی‌دهد. property ورک‌بوک AllowUnsafeFormulaCallbacks پیش‌فرضش False است، چک قبل از ارزیابی هر آرگومان اجرا می‌شود، و یک تماس ردشده xlfeUnsafeFunctionDenied گزارش می‌کند بدون اینکه حتی یک handler صدا زده شود

سناریویی که این را لازم کرد کاملاً روزمره است. یک سرویس فایل‌های XLS یا XLSX آپلودشده را می‌گیرد، سرور-سند recalculationشان می‌کند و چند جمع را برمی‌خواند. اپلیکیشن میزبان سال‌ها قبل برای دو سه تابع بیزینسی یک handler از نوع OnUserFunction رجیستر کرده بود، و در جایی از مسیر آن handler یک شاخهٔ catch-all پیدا کرده بود که هر چیزی را نمی‌شناسد به یک جدول پلاگین ارسال می‌کند. هیچ‌کس در تیم هرگز =WEBSERVICE(...) را در سلولی تایپ نکرده بود. آپلودر کرده بود. سالم نگه داشتن آن فرمول در open و recalc و save یک فیچر file-fidelity است. اما اجازه دادن به اینکه به کد میزبانی برسد که می‌تواند سوکت یا فایل باز کند یک تصمیم مجوزدهی است، و تا وقتی HotXLS این دو را جدا نکرده بود، کتابخانه بی‌سروصدا آن تصمیم را به‌جای شما می‌گرفت

چرا حفظ کردن یک فرمول تبدیل به مجوز اجرایش شد؟

ریشهٔ مشکل یک مسیر fallback تکی بود. HotXLS هر نام تابع اکسلی را که می‌شناسد پارس می‌کند، ولی هر نام شناخته‌شده پیاده‌سازی‌ای در موتور محاسبه ندارد. built-inهایی که شناخته می‌شوند ولی پیاده‌سازی ندارند قبلاً به همان fallback توابع user-defined می‌افتادند که نام‌های واقعاً سفارشی می‌افتند، پس CALL و REGISTER.ID یک مسیر dispatch مشترک با DISCOUNT یا REGIONRATE شما داشتند. نام‌های ناشناخته مثل WEBSERVICE یا DDE هم به همین شکل می‌توانستند با یک مدخل هم‌نام در رجیستری ورک‌بوک، رجیستری سراسری پروسه، یا یک event handler مچ شوند. مکانیک آن fallback در اینکه HotXLS چطور توابع سفارشی را از طریق OnUserFunction resolve می‌کند پوشش داده شده؛ مشکل این بود که هیچ‌جای آن مسیر نمی‌پرسید آیا خودِ نام چیزی است که یک میزبان عاقل اصلاً باید اجرایش کند

ترتیب dispatch برای معنای «ناشناخته» در اینجا مهم است. تماسی که موتور به‌صورت بومی ارزیابی نمی‌کند به ترتیب به بایندهای لغوی LAMBDA و LET پیشنهاد می‌شود که پشتیبانی closure در موتور فرمول HotXLS اول resolveشان می‌کند، بعد به توابع محلی ورک‌بوک که با RegisterUserFunction رجیستر شده‌اند، بعد به توابع سراسری پروسه از TXLSWorkbook.RegisterGlobalUserFunction، و در نهایت به رخدادهای OnUserFunction و OnUserFunctionEx. فقط وقتی همه‌شان رد کنند یک تابع واقعاً ناشناخته #NAME? می‌شود. هر مرحله بعد از lookup لامبدا کنترل را به کدی می‌دهد که خودتان نوشته‌اید، و دقیقاً به همین دلیل چک امنیتی باید جلوی کل زنجیره بنشیند نه داخل یک handler تکی

HotXLS چطور callbackهای فرمول ناامن را کنترل می‌کند: GetValueItemUserFunction نام را قبل از ساخته شدن آرایهٔ آرگومان‌ها یا اجرای هر resolver چک می‌کند، پس یک =WEBSERVICE(AUDIT_TOKEN()) تو در تو با lxErrorUnsafeFunctionDenied خارج می‌شود و لاگ ممیزی خالی می‌ماند، در حالی که تماس‌های امن زنجیره را از بایندهای LAMBDA و LET تا OnUserFunction می‌روند
فقط وقتی همهٔ مراحل رد کنند یک تابع واقعاً ناشناخته #NAME? می‌شود، و دلیلش همین است که چک امنیتی جلوی کل زنجیره می‌نشیند نه داخل یک handler تکی

HotXLS به‌صورت پیش‌فرض کدام نام توابع را بلاک می‌کند؟

XLSFormulaCallbackIsUnsafe در lxCalc.pas یک deny set ثابت با 20 نام دارد: 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 و FILE.DELETE. این‌ها نام‌هایی هستند که در اکسل یا زبان ماکروش، کد native لود می‌کنند، به شبکه می‌رسند، با پروسه‌های دیگر حرف می‌زنند یا فایل‌سیستم را دست می‌زنند. قبل از مقایسه، تابع whitespace اطراف را trim می‌کند، نام را uppercase می‌کند و یک پیشوند تکی _XLFN. یا _XLWS. را می‌کند، پس _xlfn.webservice که یک بیلد جدیدتر اکسل می‌نویسد مثل املای لختش گرفته می‌شود. این لیست در مرز calculator می‌نشیند نه در پارسرهای Classic و XLSX و ODS، که باعث می‌شود یک AST تکی و یک token stream تکی BIFF و یک ورک‌بوک تبدیل‌شده یکسان رفتار کنند

HotXLS چطور قبل از مقایسهٔ unsafe-callback یک نام تابع را نرمال می‌کند: whitespace بریده می‌شود، نام uppercase می‌شود و یک پیشوند تکی _XLFN. یا _XLWS. کنده می‌شود تا _xlfn.webservice مثل املای لخت گرفته شود، بعد نتیجه دقیقاً با deny set ثابت 20 نامی در lxCalc.pas مچ می‌شود
لیست نام‌هایی را در بر می‌گیرد که کد native لود می‌کنند، به شبکه می‌رسند، با پروسه‌های دیگر حرف می‌زنند یا فایل‌سیستم را دست می‌زنند، از DDE و CALL و WEBSERVICE تا FWRITE و FILE.DELETE

دو لبه قبل از اعتماد کردن به آن ارزش دانستن دارد. مچ دقیق است، پس handlerی که با نام MYWEBSERVICE رجیستر کرده‌اید تحت تأثیر نیست، و برعکس یک UDF داخلی مشروع که اتفاقاً OPEN یا RUN نام دارد حالا به‌صورت پیش‌فرض رد می‌شود. deny set هم برای handlerهای خودتان sandbox نیست. اگر شاخهٔ catch-all شما نام پلاگین‌های دلخواه را اجرا کند، گیت مشهورهای خطرناک را می‌گیرد و هیچ چیز دیگری را؛ fix ماندگار همچنان handlerی است که با SameText با یک allowlist صریح مچ می‌کند و برای هر چه مال خودش نیست Handled را روی False می‌گذارد

چرا گیت باید قبل از ارزیابی آرگومان‌ها اجرا شود؟

گیتی که بعد از محاسبه شدن آرگومان‌ها فعال می‌شود خیلی دیر است، چون خود آرگومان‌ها می‌توانند کد شما را صدا بزنند. GetValueItemUserFunction اول نام را چک می‌کند و با lxErrorUnsafeFunctionDenied خارج می‌شود، قبل از اینکه آرایهٔ آرگومان‌ها را بسازد، قبل از اینکه به سراغ resolver یا هر یک از دو رجیستری برود، و حتی قبل از اینکه بفهمد اصلاً handlerی assign نشده. همین ترتیب است که حالت تو در توی زیر را می‌شکند، جایی که تماس بیرونی به هر حال رد می‌شد ولی یک UDF درونی بی‌آزار-به-نظر وگرنه اول فعال می‌شد و side effectاش را جا می‌گذاشت

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 در کد میزبان
    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 و FAuditLog هنوز خالی است

پیش‌فرض ورک‌بوک در برابر TXLSFormulaEvaluationOptions به‌ازای هر تماس

پرچم ورک‌بوک پیش‌فرض است و گزینهٔ به-ازای-هر-تماس حرف آخر را می‌زند. TXLSWorkbook.AllowUnsafeFormulaCallbacks و TXLSXWorkbook.AllowUnsafeFormulaCallbacks بر recalculation معمولی حکم می‌رانند، یعنی Calculate و EvaluateFormulaAt دومتغیره و قالب‌های ارزیابی و viewهای فقط‌خواندنی و روی XLSX همهٔ workerهای pool recalculation موازی. هر نقطهٔ ورودی که یک رکورد صریح TXLSFormulaEvaluationOptions می‌گیرد Options.AllowUnsafeFormulaCallbacks را به‌عنوان حکم همان تماس قبول می‌کند و آن را با property ورک‌بوک OR نمی‌کند. این نامتقارنی عامدانه است: یک job داخلی مورد اعتماد می‌تواند بدون فلیپ کردن کل ورک‌بوک یک lookup از نوع RTD را مجاز کند، و ورک‌بوکی که سراسری opt-in شده باز هم می‌تواند یک ارزیابی حساس را به رد برگرداند

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // ورک‌بوک قفل می‌ماند، یک تماس مورد اعتماد عبور می‌کند
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // ورک‌بوک opt-in شده، ولی این ارزیابی از متن آپلودشده نیست
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // پرچم دوباره False است
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

فلیپ کردن property ورک‌بوک گراف وابستگی را هم روی هر دو موتور dirty علامت می‌زند. بدون این قدم، یک نتیجهٔ کش‌شده که وقتی callbackها مجاز بودند حساب شده بود می‌توانست بعد از لغو مجازشان سرو شود، یا یک نتیجهٔ کش‌شدهٔ xlfeUnsafeFunctionDenied می‌توانست از یک opt-in قدیمی‌تر بیرون بیاورد. status جدید بعد از xlfeFailed به TXLSFormulaEvaluationStatus append شده، پس ordinal آن 10 است و هر ordinal موجود مقدارش را نگه می‌دارد؛ همان قاعدهٔ append-در-انتها برای فیلد رکورد گزینه‌ها و getter و setter مربوط به IXLSWorkbook هم برقرار است، هرچند مصرف‌کننده‌ای که روی یک ریلیز قدیمی‌تر بیلد شده باز هم به recompile نیاز دارد

متن فرمول ناشناخته و ناامن موقع ذخیره چه می‌شود؟

نگه داشتن یک فرمول و اجرایش حالا دو سؤال جداست، و سیاست ورود فقط جواب اول را می‌دهد. FormulaEntryPolicy روی هر دو کلاس ورک‌بوک UnknownFunctionMode و UnknownNameMode را حمل می‌کند، هر دو با پیش‌فرض xlfusmReject، پس نسبت دادن یک فرمول با تماس ناشناخته از طریق property معمولی Formula قبل از اینکه مقدار سلول یا کش فرمول یا وابستگی‌ها عوض شوند رد می‌شود. ValidateFormulaEntry همان تصمیم را بدون side effect گزارش می‌کند. مسیرهای مورد اعتماد مثل لود فایل و کپی کردن و تبدیل فرمت از آن سیاست ورود کاربر عبور می‌کنند، چون یک پیش‌فرض سخت‌گیر هرگز نباید نمادهایی را که از قبل در فایلی که صرفاً باز می‌کنید حاضرند رد کند

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // ورود برای سازگاری
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // ذخیره می‌شود، نه مجاز
  Book.SaveAs('rates.xls');
end;

در BIFF8 کلاسیک یک تماس ناشناخته توکن مخصوص خودش را ندارد، پس HotXLS همان‌طور که اکسل توابع add-in را می‌نویسد می‌نویسد. فرمول یک توکن PtgNameX می‌گیرد ($59) که مدخل XTI آن به SUPBOOK مربوط به add-in اشاره می‌کند با هر دو اندیس sheet روی $FFFE، بعد توکن‌های آرگومان‌ها و یک PtgFuncVar با شمارهٔ تابع 255 و شمارندهٔ آرگومانی که اسلات نام را هم حساب می‌کند. بدنهٔ ExternName پشتیبان شش بایت صفر است، یک بایت طول و پرچم Unicode، نام تابع UTF-16، بعد یک فرمول دوبایتی از $1C $17، یعنی یک PtgErr حامل #REF!. writer نام‌های بلندتر از 255 کاراکتر و بیشتر از 29 آرگومان و مقصد BIFF5 را رد می‌کند. اینکه HotXLS این مدخل‌های SUPBOOK مربوط به add-in را کنار لینک‌های ورک‌بوک خارجی چطور طبقه‌بندی می‌کند در قواعد طبقه‌بندی SUPBOOK و XTI برای لینک‌های خارجی BIFF توضیح داده شده. XLSX متن خام تابع را نگه می‌دارد و ODS فرمول msoxl: خودش را، و در هر فرمتی فایلی که =WEBSERVICE(...) را ذخیره کرده با متن سالم دوباره باز می‌شود و همچنان به‌صورت پیش‌فرض به xlfeUnsafeFunctionDenied ارزیابی می‌شود

HotXLS چطور یک تماس فرمول ناشناخته را در BIFF8 کلاسیک می‌نویسد: فرمول توکن PtgNameX را حمل می‌کند که مدخل XTI آن به SUPBOOK مربوط به add-in اشاره می‌کند با هر دو اندیس sheet روی $FFFE، بعد توکن‌های آرگومان‌ها و یک PtgFuncVar با شمارهٔ تابع 255، با پشتیبانی بدنهٔ ExternName که به یک PtgErr دوبایتی $1C $17 حامل #REF! ختم می‌شود
XLSX متن خام تابع را نگه می‌دارد و ODS فرمول msoxl: خودش را، پس فایلی که =WEBSERVICE(...) را ذخیره کرده با متن سالم دوباره باز می‌شود و همچنان به‌صورت پیش‌فرض به xlfeUnsafeFunctionDenied ارزیابی می‌شود

اگر پایپ‌لاین شما ورک‌بوک‌هایی را ارزیابی می‌کند که نویسنده‌شان نیستید، AllowUnsafeFormulaCallbacks را روی False بگذارید، handlerها را روی یک allowlist صریح نگه دارید، و گزینه‌های به-ازای-هر-تماس را فقط جایی بدهید که مبدأ فرمول مال خودتان است. API کامل callback و سیاست ورود و ارزیابی همراه کامپوننت spreadsheet Delphi HotXLS مستند شده