PostgreSQL 的 pgcrypto 扩展里有一段 OpenPGP 实现,这段代码从 2005 年左右就躺在 contrib 目录中,直到 2025 年 12 月,一个堆缓冲区溢出漏洞被实际利用——整整二十年,无人察觉。
漏洞在哪里
pgcrypto 是 PostgreSQL 的 contrib 扩展,提供对称加密、哈希、PGP 加密等功能。其中 OpenPGP 相关函数(pgp_sym_encrypt、pgp_pub_encrypt 等)依赖一段手写的 C 实现,负责解析和构造 PGP 包格式。
问题出在 PGP 包的解析逻辑上。OpenPGP 规范定义了多种包类型,每种包有自己的长度编码方式——定长、不定长、部分长度等。解析器在处理某些边界条件时,对缓冲区大小的计算不够严谨,导致堆上分配的缓冲区可以被写入超出其容量的数据。典型的堆溢出:分配了 N 字节,写入了 N+K 字节。
这类漏洞在密码学代码里尤其危险,因为:
- 加密/解密操作往往处理的是敏感数据(密钥、明文)
- 堆溢出可能泄露内存中的密钥材料
- 攻击者可以构造恶意 PGP 包,通过 SQL 函数触发解析
为什么二十年没人发现
几个原因叠加在一起:
使用面窄。 大多数 PostgreSQL 用户用 pgcrypto 做的是 encrypt()/decrypt() 这种对称加密,或者 digest() 做哈希。OpenPGP 函数用的人少,攻击面自然也小。
测试覆盖不足。 contrib 扩展的测试力度远不如核心引擎。pgcrypto 的 OpenPGP 测试主要验证正常输入能否正确加密解密,对畸形输入的边界测试几乎没有。
代码路径冷僻。 PGP 包格式本身复杂,解析器里有大量只在新奇输入组合下才会走到的分支。日常使用根本不会触发溢出路径。
审计盲区。 安全审计往往聚焦核心引擎和热门扩展,一个 contrib 里的小众密码学实现很容易被跳过。
直到 2025 年 12 月,有人构造了特定的恶意 PGP 数据,通过 pgp_sym_decrypt 或相关函数触发溢出,漏洞才从理论风险变成现实攻击。
堆溢出在数据库场景下的影响
PostgreSQL 是进程模型——每个客户端连接对应一个 backend 进程。堆溢出发生在 backend 进程的内存空间中,影响范围取决于攻击者能控制什么:
| 场景 | 影响 |
|---|---|
普通 SQL 用户调用 pgp_sym_decrypt(恶意数据, ...) |
backend 进程崩溃,连接断开 |
| 能控制输入且了解堆布局的攻击者 | 可能实现代码执行,接管 backend 进程 |
| backend 进程被接管后 | 以 postgres 操作系统用户的权限执行任意代码 |
第三种情况最严重。postgres 用户通常拥有数据库文件的全部读写权限,在很多部署中还持有超级用户数据库角色。一个 backend 进程被攻破,等于整台数据库服务器沦陷。
检查你是否受影响
第一步:确认你的 PostgreSQL 是否安装了 pgcrypto,以及当前版本。
-- 检查 pgcrypto 是否已安装
SELECT * FROM pg_available_extensions WHERE name = 'pgcrypto';
-- 如果已安装,查看版本
SELECT extversion FROM pg_extension WHERE extname = 'pgcrypto';
-- 检查 PostgreSQL 版本
SELECT version();
第二步:确认是否有数据库实际在使用 OpenPGP 函数。在所有数据库中执行:
-- 搜索使用 pgp_* 函数的代码
SELECT proname, prosrc
FROM pg_proc
WHERE proname LIKE 'pgp_%'
AND prosrc IS NOT NULL;
更实用的做法是检查应用层是否调用了这些函数:
-- 搜索视图、函数、触发器中引用 pgp 函数的依赖
SELECT DISTINCT objid::regclass, objsubid
FROM pg_depend
WHERE refobjid IN (
SELECT oid FROM pg_proc WHERE proname LIKE 'pgp_%'
);
如果查询结果为空,说明你的环境中没有代码依赖 OpenPGP 函数,风险大幅降低——但扩展本身仍然存在,恶意用户如果拥有 CREATE 权限,仍可主动调用。
立即可以做的缓解措施
升级 PostgreSQL。 这是根本解决方案。2025 年 12 月之后的所有补丁版本都修复了该溢出。检查你运行的具体小版本:
# 查看 PostgreSQL 版本
pg_config --version
# 或通过包管理器
# Debian/Ubuntu
dpkg -l postgresql-*-main
# RHEL/CentOS
rpm -qa | grep postgresql
升级后重启所有 backend 进程:
# 重启 PostgreSQL(需要超级用户权限)
sudo systemctl restart postgresql
# 或者用 pg_ctl
pg_ctl restart -D /var/lib/postgresql/data
如果暂时无法升级,卸载或限制 pgcrypto。
-- 从所有数据库卸载 pgcrypto(需要超级用户)
DROP EXTENSION pgcrypto;
-- 如果某些数据库必须保留 pgcrypto 但不需要 OpenPGP 功能
-- 可以用 SECURITY LABEL 限制执行权限(PostgreSQL 9.2+)
REVOKE ALL ON FUNCTION pgp_sym_encrypt FROM PUBLIC;
REVOKE ALL ON FUNCTION pgp_sym_decrypt FROM PUBLIC;
REVOKE ALL ON FUNCTION pgp_pub_encrypt FROM PUBLIC;
REVOKE ALL ON FUNCTION pgp_pub_decrypt FROM PUBLIC;
REVOKE ALL ON FUNCTION pgp_key_id FROM PUBLIC;
-- 只授权给真正需要的角色
GRANT EXECUTE ON FUNCTION pgp_sym_encrypt TO app_writer;
用替代方案替换 OpenPGP 功能。 如果应用确实需要 PGP 加密,在应用层完成比在数据库层完成更安全:
# 应用层 PGP 加密示例(Python + python-gnupg)
# 安装:pip install python-gnupg
import gnupg
gpg = gnupg.GPG(gnupghome='/path/to/keyring')
# 对称加密
encrypted = gpg.encrypt(
"敏感数据明文",
passphrase="强密码-至少32字符",
symmetric=True,
)
# 存入数据库时只存加密后的文本
# INSERT INTO secrets (data) VALUES (%s)
cursor.execute("INSERT INTO secrets (data) VALUES (%s)", (str(encrypted),))
# 解密
decrypted = gpg.decrypt(str(encrypted_data), passphrase="强密码-至少32字符")
plain_text = decrypted.data.decode('utf-8')
应用层加密的好处:密码学库(如 GnuPG、libsodium)维护活跃,漏洞响应快;数据库进程不接触明文和密钥;即使数据库被攻破,攻击者拿到的也是密文。
从这个事件中学到什么
contrib 不是安全避风港。 PostgreSQL contrib 扩展的质量标准与核心引擎不同。使用任何 contrib 扩展前,应该像对待第三方库一样审视它——检查维护状态、测试覆盖、已知 CVE。
冷代码是高风险代码。 很少被执行的路径最容易藏漏洞。对密码学代码而言,畸形输入测试应该和正常功能测试同等重要。如果你在项目中使用了密码学功能,至少要跑一遍针对边界输入的模糊测试。
最小权限原则对扩展也适用。 不要默认把 contrib 扩展安装到所有数据库。只在实际需要的数据库中启用,只把函数权限授予实际需要的角色。
数据库内的密码学操作要慎重。 把加密放在数据库层意味着数据库进程同时持有明文、密钥和密文——这是最大的单点风险。能移到应用层的,尽量移出去。
二十年的潜伏期听起来很长,但在密码学实现的历史里并不罕见。OpenSSL 的 Heartbleed 藏了两年,DSA nonce 泄漏的各类实现问题反复出现多年。pgcrypto 这次的事件再次提醒:手写密码学解析代码是高危活动,而"没人用过"不等于"没人会攻击"。检查你的数据库,确认 pgcrypto 的状态,该升级就升级,该卸载就卸载。