HotXLS 拒绝把 20 个危险公式名——包括 CALL、REGISTER.ID、WEBSERVICE 和 DDE——路由到你的 Delphi 用户函数回调,除非你显式打开。工作簿属性 AllowUnsafeFormulaCallbacks 默认 False,检查在任何参数求值之前就跑,被拒绝的调用报告 xlfeUnsafeFunctionDenied,一个处理器都不会被调到
逼出这个功能的场景非常日常。一个服务接收上传的 XLS 或 XLSX 文件,在服务端重算,再读回几个合计数。宿主应用多年前为了几个业务函数注册了一个 OnUserFunction 处理器,途中不知哪一天,这个处理器长出了一个兜底分支,把所有不认识的名字转发给插件表。团队里没人往单元格里敲过 =WEBSERVICE(...)。上传文件的人敲了。让这条公式在打开、重算、保存之间原样保真,这是文件保真的特性;让它够到能开 socket、能碰文件的宿主代码,这是授权决策。在 HotXLS 把这两件事分开之前,库一直在悄悄替你做这个决定
为什么保留一条公式变成了运行它的许可?
根因是一条兜底路径。HotXLS 解析它认识的每个 Excel 函数名,但不是每个认识的名字在计算引擎里都有实现。那些认得却没实现的内置名过去和真正的自定义名字落进同一个用户定义函数兜底,于是 CALL 和 REGISTER.ID 与你的 DISCOUNT 或 REGIONRATE 共用一条分派路径。像 WEBSERVICE 或 DDE 这样的未知名同样可能在工作簿注册表、进程级注册表或某个事件处理器里撞上一个同名条目。这条兜底的机制在HotXLS 如何通过 OnUserFunction 解析自定义函数里讲过;问题在于这条路径上没有任何东西问过一句:这个名字本身是不是一个正常的宿主该执行的
分派顺序决定了「未知」在这里的含义。引擎原生算不了的调用会被依次递给词法 LAMBDA 和 LET 绑定——HotXLS 公式引擎的闭包支持最先解析它们——然后是 RegisterUserFunction 注册的工作簿本地函数,再是 TXLSWorkbook.RegisterGlobalUserFunction 注册的进程级函数,最后才是 OnUserFunction 和 OnUserFunctionEx 事件。只有全都拒绝之后,真正未知的函数才变成 #NAME?。lambda 查找之后的每一级都把控制权交给你写的代码——这正是安全检查必须坐在整条链前面、而不是塞进任何一个处理器里的原因
HotXLS 默认拦下哪些函数名?
lxCalc.pas 里的 XLSFormulaCallbackIsUnsafe 持有一个固定的 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。这些名字在 Excel 或其宏语言里会加载本机代码、够到网络、与其他进程对话或碰文件系统。比较之前函数会去掉首尾空白、把名字转成大写、剥掉单个 _XLFN. 或 _XLWS. 前缀,所以新版 Excel 写出的 _xlfn.webservice 和裸写法一样被抓住。这份名单住在计算器边界,而不是 Classic、XLSX 和 ODS 各个解析器里,这让一份 AST、一条 BIFF token 流和一份转换后的工作簿行为完全一致
依赖它之前有两处边界值得知道。匹配是精确的,所以你注册成 MYWEBSERVICE 的处理器不受影响;反过来,一个恰好叫 OPEN 或 RUN 的正经内部 UDF 现在默认被拒。这份拒绝集也不是给你自己处理器准备的沙箱。如果你的兜底分支会执行任意插件名,这道闸只挡住那些出名的危险名字,其他一概不管;治本的办法仍然是让处理器用 SameText 对显式白名单做匹配,对自己不拥有的名字把 Handled 留在 False
为什么闸门必须在参数求值之前跑?
参数算完之后才触发的闸门太晚了,因为参数自己就能调你的代码。GetValueItemUserFunction 先检查名字,在构建参数数组之前、在咨询任何解析器或两个注册表之前、甚至在发现根本没挂处理器之前,就以 lxErrorUnsafeFunctionDenied 退出。正是这个顺序挡住了下面的嵌套用例——外层调用反正会被拒,但若不这样,一个看起来人畜无害的内层 UDF 会先被触发,把副作用留下来
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'); // 宿主代码里的副作用
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 管着普通重算、Calculate、双参数的 EvaluateFormulaAt、求值模板、只读视图,以及在 XLSX 上并行重算池里的每个 worker。任何接受显式 TXLSFormulaEvaluationOptions 记录的入口都把 Options.AllowUnsafeFormulaCallbacks 当作该次调用的裁决,不与工作簿属性做 OR。这个不对称是故意的:可信的内部作业可以只授权一次 RTD 查询而不翻转整个工作簿,全局打开了的工作簿也能把一次敏感求值强制压回拒绝
var
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
// 工作簿保持锁定,放行一次可信调用
Book.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// 工作簿已打开,但这次对上传文本的求值没有
Book.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // 标志又回到 False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message);
end;
切换工作簿属性还会在两个引擎上把依赖图标脏。没有这一步,回调还被允许时算出的缓存结果可能在收回之后继续被端出来,或者一个缓存的 xlfeUnsafeFunctionDenied 结果会比打开开关活得更久。新状态追加在 TXLSFormulaEvaluationStatus 里 xlfeFailed 之后,所以它的序数是 10,既有序数全部保值;同样的尾部追加规则适用于选项记录字段和 IXLSWorkbook 的 getter 与 setter,不过对着旧版本编译出来的使用方仍然要重编译
未知与危险公式文本在保存时怎么样?
保留一条公式和运行它是现在两个独立的问题,入口策略只回答第一个。两个工作簿类上的 FormulaEntryPolicy 带着 UnknownFunctionMode 和 UnknownNameMode,都默认 xlfusmReject,所以通过普通 Formula 属性赋一条带未知调用的公式会在单元格值、公式缓存或依赖项变化之前被拒。ValidateFormulaEntry 报告同样的决定且无副作用。文件加载、复制、格式转换这类可信路径绕开这条用户入口策略,因为严格的默认绝不能拒掉你只是打开的文件里本来就有的符号
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 里,未知调用没有自己的 token,所以 HotXLS 按 Excel 写加载宏函数的方式写它。公式得到一个 PtgNameX token($59),其 XTI 条目指向加载宏 SUPBOOK,两个 sheet 索引都设为 $FFFE,后面跟参数 token 和一个 PtgFuncVar,函数号 255、参数计数把名字槽也算进去。背后的 ExternName 记录体是六个零字节、一个长度字节和 Unicode 标志、UTF-16 函数名,然后是两个字节的公式 $1C $17——一个装着 #REF! 的 PtgErr。写入器拒绝超过 255 字符的名字、超过 29 个参数,以及 BIFF5 目标。HotXLS 如何把这些加载宏 SUPBOOK 条目与外部工作簿链接一起分类,见BIFF 外部链接的 SUPBOOK 与 XTI 分类规则。XLSX 保留原始函数文本,ODS 保留它的 msoxl: 公式,而在每种格式里,保存了 =WEBSERVICE(...) 的文件重新打开时文本原样保留、默认仍然求值为 xlfeUnsafeFunctionDenied
如果你的流水线要重算不是自己写的工作簿,把 AllowUnsafeFormulaCallbacks 留在 False,处理器守住显式白名单,只在公式来源是你自己时才授予每次调用的选项。完整的回调、入口策略与求值 API 见 HotXLS Delphi 电子表格组件的文档