漏洞

Calc 打开表格,为什么会加载远程程序

LibreOffice 与 OpenOffice 的两项 JDBC 漏洞,让 Java 与 JDBC 可用的受影响旧版 Calc 按文档指定的位置加载数据库驱动,打开表格的电脑便可能执行远程代码。LibreOffice 当前可更新到 26.8.1,OpenOffice 的正式版 4.1.16 仍受影响,应先禁用 Java 集成。

浅暖纸手绘:一张表格通过数据库连接线,牵动了电脑内部的程序装置。
文章导航

保存了外部数据库连接、启用自动刷新的表格,下次打开时可以继续取数据,更新单元格里的内容。这项方便的功能还有一个前提,打开表格的电脑必须先装入能与数据库通信的驱动。驱动本身就是程序。

10 月 2 日,Apache 公布 OpenOffice 的 CVE-2026-59265。10 月 5 日,LibreOffice 公布 CVE-2026-63277。受影响的 Calc 允许文档指定 Java 数据库驱动及其远程位置,加载外部数据时便可能把那个位置的代码带进办公软件进程。V12 的公开演示从打开文档开始,不需要用户接受宏执行提示。

两款软件现在需要不同的处理。LibreOffice 已有正式修复包,当前下载页提供 26.8.1。OpenOffice 的正式下载仍是受影响的 4.1.16,公告中的 4.1.17 尚为候选版,Apache 建议先禁用 Java 集成。

1 表格保存了下一次连接的办法

外部数据库通常不会直接塞进单元格。表格保存一组连接信息,打开或刷新时再取数据。V12 的LibreOffice 报告和OpenOffice 报告使用的都是随表格保存的数据库范围设置。用户打开的是 ODS 电子表格,它可以引用另一份保存数据库连接配置的 ODB 文档。表格中的 table:database-range 记录要填入数据的区域,table:refresh-delay 保存刷新间隔。

以修补前的 LibreOffice 26.2.4.2 为例,xmldrani.cxx 第 258–368 行把 XML 中的数据源和 SQL 导入设置还原成内部对象,并为需要刷新且不属于选区导入的数据注册定时处理。随后,RefreshDBDataHdl()进入 DoImport(),后者创建查询对象 RowSet,由它执行查询并持有结果。程序先写入数据源名称、查询命令和命令类型,再执行连接与查询。

“数据源名称”在这里还可以是一条地址。ORowSet::calcConnection()把这个值交给数据库上下文的 getByName()。如果没有找到已注册的数据源,getByName() 第 597–649 行会把名称当作 URL,加载对应的数据库文档。打开一份 ODS 表格,后面还可能加载一份外部 ODB。

ODB 保存的也不只是数据库地址。它可以携带 JDBC 驱动的类名和类路径。JDBC 是 Java 程序连接数据库的接口,实际通信由驱动实现。文档指定驱动类名和路径,也就控制了办公软件接下来要装入哪个程序。

2 还没查到数据,驱动已经被实例化

数据库文档里的 db:java-driver-class 指定 Java 类,db:java-classpath 指定到哪里找它。LibreOffice 的数据库 XML 导入器将它们转换成连接属性,包括 JavaDriverClassPath。办公软件的 JDBC 连接封装通过java_sql_Driver::connect()接收这些配置,创建封装对象。它的java_sql_Connection::construct()再读出驱动类名和类路径,调用 loadDriverFromProperties() 准备真正的 Java 驱动。此时还没有取得数据库连接。

接下来处理的已经是代码位置。在 JConnection.cxx 第 118–241 行,loadClass()把类路径转换成 Java URL 数组,用它创建 java.net.URLClassLoader,随后加载指定类。旧版ClassPath::translateToUrls()会拆分类路径并展开内部 URL 变量,没有在创建这些 URL 前限制协议。HTTP 或 HTTPS 位置因此可以成为驱动来源。

找到类之后,loadDriverFromProperties() 在第 650 行调用 newInstanceObject(),创建驱动对象,也就是实例化。这个封装在Class.cxx 第 54–68 行先用 JNI 的 GetMethodID()取得无参构造方法,再用 NewObject()创建对象。按 JNI 规范,前一步就会初始化尚未初始化的类,后一步再执行构造。封装随后才调用 Java 驱动自己的 connect(),继续连接数据库、执行查询。恶意驱动不需要返回有用的数据表,后续“连接失败”也不能说明前面的程序没有运行。

OpenOffice 4.1.16 同样先加载类并创建驱动对象,再让驱动连接数据库。其 loadClass()创建 URL 类加载器,loadDriverFromProperties() 第 722 行创建驱动对象,再返回construct() 的后续步骤,调用 Java 驱动的连接方法。代码运行在打开文档的桌面环境里,可接触的资源受该用户的操作系统权限和其他隔离措施约束。

这条路径需要可用的 Java 运行环境及 JDBC 能力。V12 在 Windows 的 LibreOffice 26.2.4.2、Ubuntu 的 24.2.7.2,以及 Linux 和 Windows 的 OpenOffice 4.1.16 上报告了验证结果。Windows 的 OpenOffice 是 32 位程序,需要相容的 32 位 JRE。安装缺少这些组件时,链条可能提前停下。

3 两个入口,汇到同一个驱动加载器

LibreOffice 公告点名的入口是 calcext:data-mappings 中的 SQL 数据提供器,和前面的数据库范围刷新有不同的前半程。修补前的ScXMLMappingContext恢复文档指定的提供器,结束导入时立即刷新。工厂函数可据此创建 SQLDataProvider。

这个提供器在 SQLFetchThread::execute()中分开表名与数据库名称,再调用 DatabaseContext::getByName()、连接数据源和执行查询。它在数据库上下文处接入前面那条链,后续仍可能加载 ODB 指定的 JDBC 驱动。一个入口来自数据库范围的刷新,另一个来自扩展数据映射的导入,二者是不同的 XML 功能。

数据库范围刷新和 LibreOffice SQL 数据映射可以加载 ODB 连接文档,按类名及类路径装入并实例化 JDBC 驱动,随后才进行数据库查询。
图 1:受影响版本的简化执行顺序。右侧 SQL 数据映射是 LibreOffice 的入口,两条路径在数据库上下文处汇合。查看大图

修补也对应着不同位置。LibreOffice 26.2.5.2 的映射导入代码只恢复 CSV、HTML 和 XML 提供器,排除了 SQL,并移除了导入结束时的立即刷新。驱动加载的下游同时增加了独立检查。classpath.cxx 第 79–87 行在内部 URL 展开之后解析协议,要求它为 file,否则抛出异常。

// LibreOffice 26.2.5.2,classpath.cxx 关键判断
if (!uriRef.is() || !uriRef->getScheme().equalsIgnoreAsciiCase("file"))
{
    throw css::lang::IllegalArgumentException(
        "non-local Java class path entry: " + url,
        css::uno::Reference< css::uno::XInterface >(), 0);
}

检查放在 URL 展开之后,限制的是最终交给 Java 的地址。这里的“展开”只是在 URL 中替换内部变量,和 Office 宏脚本是不同功能。限制 HTTP、HTTPS 等驱动来源,也保留了 JDBC 的合法用途,预先安装并由允许类路径找到的驱动仍可使用。

Apache 公布了两项修补。第一项在 DoImport() 处理 URL 数据源前查询文档的 XLinkAuthorizer,该接口存在且拒绝授权时停止导入。第二项的类路径检查允许 file:、jrt:、jmod: 及以这三者为内层协议的 jar: 地址,其他条目记日志并跳过。LibreOffice 拒绝非 file 协议并抛错,OpenOffice 使用这组允许前缀并跳过拒绝项,具体行为不同。

JAR 是保存 Java 类的归档文件,jar: 地址在外层表示归档,里面还包含归档本身的位置。V12 使用的远程类路径是 jar:http: 这种组合。换成无害示例,jar:https://example.invalid/driver.jar!/的外层是 JAR,内层仍从网络取文件,两组新规则都会排除它。file:///drivers/example.jar符合两组协议规则,jar:file:///drivers/example.jar!/则只符合 Apache 的允许前缀。SOSEC 的字符串对照覆盖了这些形式、大小写和变量展开后的地址,结果符合所读规则。它只检查协议判断,未运行 Java URL 解析、类加载或完整 Office 产品,前面的代码执行结果来自 V12 的公开验证。

4 一个已经能升级,另一个还要先关 Java

LibreOffice 的首修版本是 26.2.5 和 26.8.0。26.2.5 发行说明记载它在 7 月 23 日已经发布,早于 10 月 5 日的安全公告。到本文 10 月 13 日核对时,官方当前下载为 26.8.1,应使用完整正式包。由发行版维护的 Linux 包则应核对发行版对应的修补记录,不能只拿上游版本号判断是否回补。

OpenOffice 的正式下载页仍提供 4.1.16,Apache 公告仍将 4.1.17 说明为发布候选。源码已有修补,用户拿到的正式包仍在公告列出的受影响范围内,等待正式修复期间应关闭 Java。

OpenOffice 的设置在“工具 → 选项 → OpenOffice → Java”,取消“使用 Java 运行环境”,macOS 从 OpenOffice 偏好设置进入相应项目。若业务无法禁用,Apache 的替代措施是不要打开不可信文件。LibreOffice 的 Java 开关位于“LibreOffice → 高级”设置,也有助于识别哪些桌面确实启用了运行环境。关闭 Java 会影响依赖 JDBC 或其他 Java 组件的业务,实施前应以正常数据库连接核对影响,并重启应用确认设置生效。升级遇到兼容问题时,先维持 Java 禁用或不处理不可信文件的限制。LibreOffice 可改用经业务验证的已修复正式构建,历史下限为 26.2.5 或 26.8.0。OpenOffice 在正式修复发布前,退回 4.1.16 仍需保留临时限制。

5 连接失败之后,还要看驱动是否加载过

更新后先确认实际打开文档的安装目录和构建,再用正常文件检查数据导入。需要 JDBC 的桌面应验证允许位置的已知驱动仍可使用,并在隔离环境核对不允许的远程类路径被拒绝。查看拒绝发生在哪个阶段,才能分清来源检查生效,还是驱动已运行、后续连接才失败。

如果旧版软件已经打开过可疑文件,调查对象包括那份表格、其引用的 ODB、驱动地址及打开期间的进程和网络记录。访问 ODB 只能证明连接链向前走了一步,取得 JAR 也不能单独证明驱动已实例化。出现 Java 代码执行或异常子进程证据时,再按办公软件用户能接触的文件、会话和凭据范围处理恢复。原始文档和遥测应先保存,避免重新打开它来“看看还会不会报错”。