导语:PostgreSQL vacuumdb 提权漏洞(CVE-2026-116313)是近期数据库安全领域一个极具代表性的“信任边界错位”案例——它并不依赖内存破坏或协议缺陷,而是利用 PostgreSQL 的 search_path 解析机制,让一个本应只读统计信息的维护工具,变成了数据库属主向操作系统用户越权的跳板。本文以一线架构师视角,完整拆解这条攻击链的每一环,厘清 18.7 的修复边界,并给出可直接落地的生产应急加固方案。
一、PostgreSQL vacuumdb 提权漏洞的本质:一次 search_path 信任错位
先把结论摆在最前面:CVE-2026-116313 的根因不是 SQL 注入,也不是权限模型缺陷,而是“不可信搜索路径”(Untrusted Search Path)这一经典 CWE-426 模式在 PostgreSQL 客户端工具上的又一次复现。vacuumdb 在内部执行 --missing-stats-only 相关的目录查询时,会以当前连接的 search_path 解析操作符(operator);而 search_path 中可能包含数据库属主可写的 schema,于是属主可以预先放置一个同名操作符进行“影子覆盖”(shadowing),从而劫持查询语义。
关键在于执行身份的错配:vacuumdb 是一个操作系统层面的客户端进程,它通常以某个系统用户(例如 postgres 或运维账号)身份运行,并携带该用户的文件系统权限与环境变量。一旦查询被劫持,攻击者构造的 SQL 便以这个系统用户身份执行,形成从“数据库属主”到“操作系统用户”的横向越权。这正是 CVSS 7.2 的评分逻辑所在——需要较高权限前提(属主),但一旦成立,影响范围跨越了数据库与操作系统两层信任边界。
不要把数据库客户端工具视为“只读、无害”的运维脚本。任何以高权限系统用户运行、且会解析用户可控 schema 内对象的工具,都必须按“可被提权利用”来建模。这是本次漏洞给所有 DBA 与平台架构师最重要的一条设计原则。
企业级架构规范
flowchart TD
subgraph 攻击面 ["攻击者可控层 (Attacker Controlled Tier)"]
A["数据库属主 (DB Owner)"] --> B["在可写 schema 中创建同名操作符"]
end
subgraph 触发层 ["工具调用层 (Tool Invocation Tier)"]
B --> C["运维以系统用户运行 vacuumdb"]
C --> D["执行 --missing-stats-only 目录查询"]
end
subgraph 解析层 ["名称解析层 (Search Path Resolution Tier)"]
D --> E["search_path 解析操作符"]
E --> F["命中影子操作符 (Shadowing Operator)"]
end
subgraph 越权层 ["提权执行层 (Privilege Escalation Tier)"]
F --> G["以系统用户身份执行任意 SQL"]
G --> H[("文件系统 / 环境变量 / 外部命令")]
end
二、攻击链逐环拆解:影子操作符如何劫持目录查询
2.1 为什么是操作符,而不是函数或表
PostgreSQL 的 search_path 解析顺序对表、函数、操作符、类型都有影响,但操作符的解析有一个常被忽视的特性:它不要求显式 schema 限定,且解析时会在 search_path 中按顺序查找匹配的操作符签名。这意味着只要属主在 search_path 靠前的 schema 中创建一个签名兼容的操作符,就能“遮蔽”系统 schema 中的同名操作符。相比函数劫持,操作符劫持更隐蔽,因为很多审计规则只关注函数与表。
- 前提条件一:目标数据库的 search_path 包含属主可写的 schema(如 public 或业务 schema)。
- 前提条件二:vacuumdb 以具备较高系统权限的用户运行。
- 前提条件三:调用了
--missing-stats-only路径下的目录查询。
2.2 pg_upgrade 文档中的“推荐调用”是放大器
本次漏洞最值得警惕的一点是:pg_upgrade 的官方文档推荐了一种受影响的 vacuumdb 调用方式。这意味着大量遵循官方升级手册的团队,在大版本升级流程中会“合规地”触发这条危险路径。攻击者无需诱导运维执行奇怪命令,只需在升级窗口前埋好影子操作符,即可在标准升级流程中完成提权。这是供应链式攻击思维在数据库运维场景的典型体现。
如果你的升级 Runbook 中直接复制了 pg_upgrade 文档里的 vacuumdb 命令,且该命令以 postgres 系统用户执行、连接的是业务库(search_path 含 public),那么你的升级窗口就是一次潜在的提权窗口。请立即核对 Runbook 并加入 search_path 收紧步骤。
2.3 版本影响边界:18.x 的精确切片
漏洞的影响范围非常“干净”:PostgreSQL 18 主版本中,18.7 之前的次版本受影响;18 之前的版本不受影响。这种切片说明相关代码路径是在 18 引入的(与 --missing-stats-only 的目录查询实现相关)。对于仍停留在 16、17 的团队,这是一个“意外的好消息”,但也提醒我们:大版本升级不仅是功能升级,更是攻击面重置。
| 版本区间 | 是否受影响 | 处置建议 | 风险等级 |
|---|---|---|---|
| PostgreSQL < 18 | 不受影响 | 无需紧急升级,保持常规补丁节奏 | 低 |
| PostgreSQL 18.0 – 18.6 | 受影响 | 立即升级至 18.7 或应用缓解措施 | 高 |
| PostgreSQL 18.7+ | 已修复 | 确认版本并复核 search_path 配置 | 已缓解 |
三、应急响应:从检测到加固的四步落地
3.1 第一步:资产盘点与版本确认
先确认你是否有 18.x 实例,以及次版本号。不要依赖“我记得是 18.5”这类记忆,直接查。
-- 确认服务端精确版本
SELECT version();
SELECT current_setting('server_version_num')::int AS version_num;
-- 18.7 对应 version_num = 180007
3.2 第二步:排查可疑影子操作符
核心检测思路是:找出定义在非系统 schema 中、且与系统操作符签名冲突的对象。以下查询可快速定位高风险候选。
-- 列出所有非 pg_catalog 中定义的操作符及其所属 schema
SELECT n.nspname AS schema_name,
o.oprname AS operator_name,
o.oprleft::regtype AS left_type,
o.oprright::regtype AS right_type,
pg_get_userbyid(o.oprowner) AS owner
FROM pg_operator o
JOIN pg_namespace n ON n.oid = o.oprnamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schema_name, operator_name;
若在业务 schema 或 public 中发现与 pg_catalog 同名同签名的操作符,且属主为普通数据库用户,应视为已被植入或存在植入风险,立即隔离该对象并启动取证。不要简单 DROP 了事——先导出定义与属主信息作为证据。
3.3 第三步:收紧 search_path(最有效的临时缓解)
在无法立即升级的窗口期,收紧 search_path 是最直接、最有效的缓解手段。原则是:让 pg_catalog 优先,且不把可写 schema 放在解析链前端。对于运行 vacuumdb 的会话,建议显式设置。
同时,对数据库级别与角色级别做统一约束,避免个别连接遗漏:
-- 数据库级默认 search_path 收紧 ALTER DATABASE your_db SET search_path = pg_catalog, public; -- 关键运维角色强制收紧(示例) ALTER ROLE postgres SET search_path = pg_catalog, pg_temp; -- 复核当前生效值 SHOW search_path;
search_path 加固不是一次性动作,而应纳入数据库基线配置(Baseline Configuration)管理。建议在 CI 或配置审计中,将“search_path 首项必须为 pg_catalog”作为强制校验项,防止后续变更回退。
3.4 第四步:升级至 18.7 并复核 pg_upgrade 流程
临时缓解只是止血,根治必须升级到 18.7。升级后,务必回头修订 pg_upgrade Runbook:在调用 vacuumdb 前显式设置安全 search_path,或改用带 schema 限定的等价命令。以下为升级与验证的参考命令。
点击展开:升级执行命令与升级后验证脚本
# 1) 备份并确认当前版本
pg_dumpall -U postgres > /backup/pre_upgrade_$(date +%F).sql
psql -U postgres -c "SELECT version();"
# 2) 升级 PostgreSQL 18.x 至 18.7(以 Debian/Ubuntu 为例)
sudo apt-get update
sudo apt-get install --only-upgrade postgresql-18
# 3) 重启并验证版本
sudo systemctl restart postgresql
psql -U postgres -c "SELECT current_setting('server_version_num')::int;"
# 期望输出 >= 180007
# 4) 复核 search_path 与可疑操作符
psql -U postgres -c "SHOW search_path;"
psql -U postgres -d your_db -f detect_shadow_operators.sql
四、从单点漏洞到系统性防御:数据库运维的信任边界治理
PostgreSQL vacuumdb 提权漏洞的价值,远不止于一个 CVSS 7.2 的补丁。它揭示了一个被长期低估的系统性风险:数据库客户端工具与数据库服务端共享同一套名称解析语义,却运行在不同的权限域中。只要这两个域之间存在“用户可控对象”的注入点,提权链就成立。要系统性防御,需要从三个层面治理。
- 权限最小化:运行 vacuumdb、pg_upgrade 等维护工具的系统用户,不应具备超出数据库维护所需的文件系统与进程权限;能专用账号就不要用 root 或万能运维账号。
- 解析链固定:所有自动化运维脚本在连接数据库时,显式设置 search_path,禁止依赖实例默认值;将 pg_catalog 置于首位作为硬性规范。
- 对象变更审计:对 pg_operator、pg_proc、pg_class 等系统目录的 DDL 变更建立审计与告警,尤其是非系统 schema 中与系统对象同名的创建行为。
| 防御层次 | 传统做法 | 现代加固方案 | 架构收益 |
|---|---|---|---|
| 身份与权限 | 统一用 postgres 系统用户跑所有工具 | 专用低权限运维账号 + sudo 白名单 | 越权面收敛 |
| 名称解析 | 依赖实例默认 search_path | 脚本显式设置 pg_catalog 优先 | 劫持链阻断 |
| 对象审计 | 事后人工排查 | 系统目录 DDL 变更实时告警 | 植入可发现 |
| 补丁管理 | 季度集中升级 | 安全公告驱动的紧急升级通道 | 窗口期缩短 |
最后给出一条架构师视角的判断:这类漏洞的修复成本很低(一次小版本升级 + 一条配置),但它的检测成本与取证成本很高。因此真正的工程投入应放在“可观测性”上——让影子操作符的创建行为在发生的那一刻就被记录与告警,而不是等到升级窗口出事后再回溯。把 PostgreSQL vacuumdb 提权漏洞当作一次基线治理的契机,远比把它当作一个孤立 CVE 更有价值。
本文核心技术背景源自海外一线安全公告与漏洞数据库:
OSV – UBUNTU-CVE-2026-116313 (PostgreSQL vacuumdb Untrusted Search Path)
