周二下午,安全群突然弹出一条公告:CVE-2025-1094,PostgreSQL系数据库的libpq转义函数存在SQL注入风险,受影响范围覆盖PG 13到17全系,以及所有基于PG内核分叉的衍生数据库。我们这边维护的GaussDB集群赫然在列。第一反应是去社区拉修复PR,cherry-pick、编译、打包、上线,一气呵成。结果第二天业务侧就炸了:一批包含反斜杠的查询结果开始不一致,某些存储过程报语法错误,连慢查询曲线都跟着飘了起来。
这篇文章就是想把这次"合入PG上游PR修复CVE-2025-1094之后引发的隐患"完整复盘一遍。内容主要面向GaussDB、PostgreSQL以及各类PG衍生数据库的运维、内核研发和应用开发人员。如果你也打算直接把社区补丁平移到自己的分支上,我建议你先花十分钟看完这篇,搞清楚这个PR到底改了什么、它依赖了哪些上游假设、在GaussDB这种带着历史兼容包袱的分支上又会踩到哪些坑。
1. CVE-2025-1094到底是个什么漏洞:一个字节截断引发的注入危机
1.1 漏洞的根源:PQescapeLiteral的反斜杠处理逻辑
CVE-2025-1094出在libpq的PQescapeLiteral和PQescapeIdentifier这两个函数上。它们的作用很朴素:应用程序拼SQL之前,把用户输入的单引号、反斜杠等特殊字符转义掉,防止输入内容破坏原有的SQL结构。几乎所有主流语言的PG驱动最终都在调用这一层逻辑——psycopg2、JDBC、ODBC,底层走的都是libpq的转义能力。所以这个函数一旦出问题,影响面是整个生态。
漏洞的核心触发条件是standard_conforming_strings处于关闭状态,也就是数据库运行在旧式字符串语义下。在这个模式下,字符串字面量里的反斜杠本身是转义字符,\'会被解释成单引号、\\会被解释成反斜杠。问题在于,PQescapeLiteral的旧实现里,转义逻辑对反斜杠的检查是非穷尽的——它默认"用户输入里反斜杠不常见",于是对某些特殊构造的处理出现了字节截断。攻击者可以利用这一点,构造一个输入让转义后的字符串在某个多字节序列边界处被错误切断,导致后面紧跟的单引号逃逸出字符串边界,重新获得SQL注入的能力。
需要特别强调的是,多字节编码(比如SJIS、BIG5、GBK这类字符编码中,部分字符的最后一个字节正好是0x5C,也就是ASCII反斜杠)会显著放大这个问题。当服务端server_encoding配置成这类编码时,转义函数在逐字节扫描过程中可能把一个多字节字符的尾字节误判成反斜杠,做了错误的转义处理,从而破坏整个转义结果。这也是为什么这次CVE会被评为严重级别——它不是教科书里那种需要特殊配置才能触发的漏洞,而是不少老系统在生产环境里真实存在的状态。
1.2 社区PR修了什么:修复策略与潜在假设
社区的修复PR核心思路并不复杂:补强PQescapeLiteral对反斜杠的检查逻辑,同时针对多字节字符边界做了显式的字节序列判断,确保在任何standard_conforming_strings取值下,转义结果都不会被字节截断问题破坏。补丁本身在PG主线上看起来无懈可击,测试覆盖也补得很全,CVE复现用例、多字节编码用例、边界用例一应俱全。
但问题恰恰出在"主线无懈可击"上。这个PR隐含了三个重要假设:
standard_conforming_strings = on是主流运行状态,off只是历史遗留的少数派场景;- 数据库内核中,字符串字面量的解析规则和PG主线保持一致,不存在本地化的改写逻辑;
- 上层驱动和中间件没有对转义结果做二次处理,补丁前后的转义语义变化能被应用层完整感知并兼容。
这三个假设在PG主线上成立,但在GaussDB这类从PG fork出来、带着大量自研改动和兼容模式的分支上,每一个都站不住脚。这就是"合入上游PR"这个操作真正的风险来源。
2. GaussDB合入PG PR之前,必须看清的三个差异
2.1 默认GUC和兼容模式差异
GaussDB从PG 9.2那个年代分叉出来,后续又发展出自己的兼容模式体系——我接触到的部署环境里常见的有A兼容模式(Oracle风格)、B兼容模式(MySQL风格)和PG兼容模式。不同兼容模式对字符串字面量的处理并不完全相同,尤其是在standard_conforming_strings、escape_string_warning、backslash_quote这几个和转义语义强相关的参数上,不同模式之间可能有不同的默认值。
PG主线从9.1开始就把standard_conforming_strings默认值改成了on,含义是"字符串字面量里的反斜杠就是普通字符,不再作为转义符"。但GaussDB部分兼容模式为了保持对老应用的语法兼容,会把这个参数默认拨回off,或者允许会话级随意切换。这就带来一个直接后果:上游PR里精心设计的"当standard_conforming_strings = off时,把反斜杠一并转义"的逻辑,在GaussDB上不是一条冷路径,而是热路径——当大量存量SQL都跑在旧式字符串语义下,补丁的行为变化会被成倍放大。
另一个容易忽略的参数是backslash_quote。它控制的是在字符串字面量中是否允许\'这种写法,默认值是safe_encoding,即在多字节编码下会额外检查。GaussDB的某些兼容模式对backslash_quote的处理和PG主线并不完全一致,而社区的修复PR恰恰在\'的处理上做了行为调整。参数默认值不一致,就意味着同样的SQL在PG上安全、在GaussDB上可能报错,或者反过来。
2.2 fork分叉后的代码上下文差异
这是我觉得最需要提醒的一点:GaussDB不是简单换个壳的PG,它的内核里叠加了大量自研改动——安全机制、权限体系、存储引擎适配、甚至对libpq内部函数的重构。社区PR是基于PG主线当前代码上下文写的,它假设PQescapeLiteral周围还是上游那套代码结构。
实际cherry-pick的时候,大概率会遇到上下文冲突。冲突可以手动解决,但真正的隐患是"解决冲突时你以为对齐了、其实没有"。举个例子:如果GaussDB本地的libpq已经对PQescapeLiteral做过一次自研改造——比如为了适配某种网关协议,对转义结果做了额外的编码转换——那么再合入社区PR,就可能出现叠加转义:反斜杠先被社区逻辑转义一次,又被本地逻辑转义一次,最终落到SQL里的字符串值和应用原本期望的完全不是一回事。
还有一个常见问题:上游PR可能依赖了一些GaussDB分支里根本不存在的上游前序commit。比如这个修复补丁用的是某个重构后的工具函数,那个重构只在PG主线的某个大版本里出现过,GaussDB这个分叉点早于那次重构,于是你合入PR时必须把这个工具函数一起搬过来,甚至还得搬它依赖的更底层代码。牵一发动全身,就是这么来的。
2.3 周边生态和上层驱动差异
GaussDB的生态和PG还有一个本质差别:上层接入方式更多样。除了libpq原生的C接口,很多生产系统走的是GaussDB自研的JDBC驱动、ODBC驱动、或者经过中间件(sharding proxy、读写分离组件)再连到内核。这些驱动层和服务端之间,往往有自己的转义和协议处理逻辑。
社区PR只改了libpq的PQescapeLiteral,但它没法约束驱动层的行为。实际造成双重转义的场景很常见:应用层先调用PQescapeLiteral对输入做了转义,新补丁下输出的字符串已经带了反斜杠;然后驱动层出于自己的防护机制,又对整条SQL做了一次转义。两次转义叠加,SQL里的字符串值和应用想要的值就出现了偏差。表现到业务层,可能是"数据存进去是对的、查出来多了个反斜杠",也可能是"同样的SQL,升级前能查到数据、升级后查不到了"。
这类问题最麻烦的地方在于:它不报错、不崩溃,只是静默地改变数据行为。很多时候DBA盯了两天监控都找不到异常,最后发现是业务方反馈"导出的CSV里反斜杠数量不对"才顺藤摸瓜定位到转义变化。
3. 合入后容易爆发的三类严重隐患:行为偏移、注入面残留、数据不一致
3.1 隐患一:双重转义导致业务数据被"改写法"
先看一个典型的触发场景。假设应用里有一段代码,用PQescapeLiteral处理一个Windows风格的文件路径C:\temp\file,然后拼进SQL去更新某张配置表。
补丁前的行为:standard_conforming_strings = off模式下,转义函数认为反斜杠本身就是转义符,它会输出类似C:\temp\file的内容,数据库解析时正好还原成C:\temp\file。
补丁后的行为:为了修复漏洞,转义函数会额外对反斜杠再做一层转义,输出变成C:\\temp\\file(具体表现取决于补丁实现和是否带E前缀)。这在standard_conforming_strings = on的PG主线上是安全的,因为反斜杠不再被特殊处理。但GaussDB上如果会话还处于off模式,\\会被进一步还原成\,数据库里实际存的还是C:\temp\file,看起来没问题。
可如果中间再隔一层驱动,驱动也做了转义,最后落到内核的SQL就变成了C:\\temp\\file直接入库存。读出来的时候,应用拿到的是两个反斜杠。配置表里的路径值全部静默变化,文件访问失败,正则表达式匹配错误,索引的key也变了——这类问题排查起来,光看应用代码是看不出来的,必须从SQL层抓实际执行的语句才能发现转义叠加了。
更隐蔽的是E前缀字符串。社区PR在部分路径下会生成E'...'格式的转义字符串,表示这是一个"转义字符串字面量"。PG主线完全认识E''语法,但GaussDB的某些兼容模式对E''的支持并不完整,或者数据库端参数backslash_quote限制导致E''内部的写法直接报错。于是,补丁明明修的是"转义不完整",在GaussDB上却表现为"大量包含特殊字符的业务SQL开始语法报错"。
3.2 隐患二:有的攻击面修了,新的绕过路径又出现
这个隐患比数据不一致更危险,因为它直接关系安全。社区PR的修复范围是PQescapeLiteral和PQescapeIdentifier两个函数。但GaussDB服务端的SQL处理链路并不是完全从libpq进来的——存储过程内部拼接SQL、自研的SQL改写器、某些兼容模式下对特殊语法做的预解析,这些路径并不都经过libpq的转义逻辑。
补丁合入后可能出现一种割裂状态:通过libpq进来的外部输入被很好地转义了,但GaussDB内部自研路径处理的字符串没有同步修复。攻击者如果找到了一个能触发内部路径的入口,完全可以绕过修复后的libpq,重新回到注入状态。这类"修了主入口、漏了侧门"的问题,在分支数据库上特别容易出现,因为上游PR根本没有你的内部代码上下文。
还有一个反向问题:修复PR改变了转义结果后,如果驱动层或者应用层不认识新的转义格式——比如驱动对E''前缀字符串不做识别、直接把E当普通字符——那么原本被安全转义的单引号可能在驱动层被错误还原,等于把补丁的防护效果又给抹掉了。这才是最讽刺的画面:你装了安全补丁,但因为上下游语义不一致,实际攻击面并没有收窄,反而因为所有人都以为漏洞修好了而放松了警惕。
3.3 隐患三:告警风暴和排查困难
合入PR之后的一段时间里,生产日志可能会出现大量escape_string_warning告警。这个告警的本意是提示"你正在使用不推荐的反斜杠转义写法,请迁移到标准字符串模式"。补丁前这种告警也有,但频率可控;补丁后因为反斜杠的处理路径变化,触发条件变多,告警量可能直接翻好几倍。
告警本身不致命,但它会掩盖真正的问题。我曾经见过一个集群,升级后三天内的错误日志几乎全是转义告警,结果把一条真正因为双重转义导致的SQL语法错误完全淹没了。等业务方报障时,日志检索已经变得非常困难——把几千条同类告警里翻出那几条关键错误,耗费的时间远超预期。
还有一类影响容易被忽略:SQL语句文本变化会导致执行计划缓存失效。补丁后,应用层传进来的SQL虽然逻辑没变,但字面量部分的转义变了(多了反斜杠、多了E前缀),数据库端的plan cache命中率可能断崖式下跌。短时间大量重复SQL重新走解析、生成执行计划的流程,慢查询量上升,CPU和内存占用也会跟着飙。如果应用有自研的SQL文本归一化逻辑,这类变化还可能影响它自己的缓存命中,推高到数据库端的连接数和QPS。
4. 把补丁安全落地:从评估到灰度的一整套操作
4.1 上线前的四项关键检查
如果你现在也面临"要不要合入这个修复PR"的决策,我给出一份可以直接照着做的检查清单。这套流程不只是针对CVE-2025-1094,任何从PG主线往GaussDB类分支合入的安全补丁,都可以复用。
第一,确认标准字符串模式的实际运行分布。在测试环境甚至生产环境的只读副本上执行下面的查询,看看各个模式的比例:
SELECT name, setting, source, sourcefile FROM pg_settings WHERE name IN ('standard_conforming_strings', 'escape_string_warning', 'backslash_quote');再检查一下连接池配置和典型会话的SHOW standard_conforming_strings;输出。如果你的业务会话绝大多数是on,风险相对可控;如果有大量off会话,请务必进入下面的兼容性测试,最好先做一轮应用层改造评估。
第二,全链路盘点转义路径。不要只盯着libpq,要画出完整的SQL流转图:应用用什么语言、走什么驱动、驱动后面有没有中间件、中间件是否做SQL改写、改写器是否涉及转义。对每条路径确认一个关键问题:补丁前后,同一段含反斜杠/单引号/E字面量的输入,最终落到内核的SQL文本差异是什么?最好在测试环境开启数据库端log_statement = 'all',实际抓一轮对比。
第三,设计回归用例集。重点覆盖以下场景:含反斜杠的普通字符串、Windows路径、正则表达式、JSON内嵌字符串、含\'的旧式写法、多字节编码(GBK/SJIS)下以0x5C结尾的字符、E''前缀字符串、存储过程和触发器内的动态SQL。每个用例都要在补丁前后各跑一遍,比对结果集。
第四,准备回滚预案。PQescapeLiteral的修复涉及C代码,意味着补丁升级后不支持简单的配置回滚,必须保留旧版本的二进制文件。确认你的部署平台支持快速切换回旧版本,并且旧版本的备份、安装包、依赖库都完整可用。没有这一步,一旦线上出了兼容性问题,你只能硬着头皮在线修,那压力就完全不一样了。
4.2 灰度发布和验证策略
安全补丁的灰度思路和功能变更不一样:功能变更可以按业务重要性分阶段放量,安全补丁往往会因为"高危漏洞必须尽快全量修复"被压缩灰度时间。我的建议是,可以加速,但至少保留三个阶梯:
- 第一阶梯:测试环境+UAT环境,跑完整回归用例集,确认数据行为一致;
- 第二阶梯:只读备机或只读业务节点,让真实流量读一段时间的库,重点比较备份/导出数据的反斜杠表现,以及日志里的转义告警量;
- 第三阶梯:少量低风险业务先切到新版本,观察24小时,重点是慢查询变化、SQL语法错误率、应用层报障量。
灰度期间要盯的监控指标,我建议做成一张清单:
| 指标 | 关注点 | 异常信号 |
|---|---|---|
| 转义告警量 | escape_string_warning频率变化 | 补丁后告警量陡增 |
| SQL语法错误率 | 数据库错误日志中语法错误占比 | 出现E''相关报错 |
| 慢查询分布 | 相同SQL的执行计划变化 | plan cache命中率下降 |
| 应用结果返回差异 | 含反斜杠字段的前后比对 | 多/少反斜杠 |
| 索引扫描效率 | 涉及转义字段的查询路径 | 原先走索引、现在变全表扫描 |
测试环境抓到的实际SQL是很有价值的一手资料。拿一条典型的、含C:\path这类数据的查询语句,把补丁前和补丁后log_statement = 'all'记录下来的完整SQL文本放在一起diff,你就能直观看到转义函数到底做了什么改变,比看源码更高效。
关于应用侧兼容,这里想多说一句。如果双重转义问题确认存在,最稳妥的方向不是在内核层继续打补丁找平衡,而是把应用从"逐字转义再拼SQL"改为参数化查询或预编译语句。参数化查询通过协议层面绑定参数,根本不依赖字符串转义来保证安全,既彻底绕开了PQescapeLiteral的行为差异,也从根上杜绝了SQL注入。这需要应用团队配合改造,但作为一次性的投入,比每一次上游补丁都跟着踩坑要划算得多。
4.3 临时缓解与替代方案
如果评估后发现当前还不能直接合入这个PR,比如关键业务没法停、应用改造周期太长、或者GaussDB版本的内部上下文和上游补丁冲突太大,那可以先用下面这些措施把风险压低:
- 把数据库端的
standard_conforming_strings强制改为on,并让连接池在建立新会话时统一设置。配合escape_string_warning = on,让存量应用里的反斜杠转义用法尽快暴露出来。 - 设置
backslash_quote = safe_encoding(如果当前版本支持),防止多字节编码下的注入绕过。注意这个参数本身就是CVE-2025-1094涉及的一部分,需要确认GaussDB对应版本对该参数的处理是否和PG一致。 - 在数据库前端的网关或中间件上,对进入的SQL做一轮额外的转义校验。这层防护是过渡性质,不能替代内核修复,但能在补丁合入前降低真实攻击的成功率。
- 如果确实需要在未修复版本上继续运行,建议暂停一切面向公网的写接口,或者至少对写操作增加独立的输入校验逻辑。
需要提醒的是,"临时缓解"不等于"安全"。上面几个措施都只降低了被利用的概率,并没有消除漏洞本身。最终还是要回到合入修复PR那条路。只是合入之前,把该做的兼容性评估做扎实,把回归用例跑透。
5. 这次事件带来的几个真实教训
5.1 上游PR不等于安全承诺
社区PR的测试覆盖再全,也是基于PG主线这个单一上下文的。PG的衍生数据库里,有像GaussDB这样大量自研改动的,有只改品牌名的,有按行业需求定制了安全模块的——每一类分支和上游的偏离程度都不同。把上游PR当成"权威参考"而不是"直接交付物",是分支数据库团队必须建立的意识。
合入一个安全PR,本质上是做一次小规模的功能开发,不是一次简单的代码同步。你需要理解补丁作者的每个意图,确认这些意图在你自己的代码上下文里依然成立,再动手合。
5.2 安全修复的验收标准应该是"业务行为不变+攻击面收窄"
漏洞修复做得对不对,不能只看"PoC跑不跑了"。安全团队给的验收标准,我强烈建议加上第二条:修复前后,所有正常业务SQL的返回结果和性能特征保持一致。也就是说,验收要同时过两关——安全测试证明攻击路径被堵住了,回归测试证明业务路径没有被改变。只过了第一关就放行,本质上还是在赌。
针对转义类函数,我推荐把这些基线用例固化成自动化测试,每次内核升级、每次参数变更都自动跑一遍,而不是只在这次CVE修复时手动执行。转义语义的回归测试投入不大,但产出非常可观。
5.3 环境越老,越要做回归
GaussDB这类老分支的问题在于,它的存量系统里有大量依赖旧语义的业务——旧式字符串处理、非标准转义、历史兼容模式,这些恰恰是CVE-2025-1094的触发温床。上游修复PR设计时默认的"标准模式是常态"假设,在老分支上直接失效。
所以越是老环境,安全修复的兼容风险越大,越不能只patch不回归。我个人现在的习惯是:拿到上游PR先不急着合,给PR里的每个修改点建立"影响面映射",也就是这行代码改完之后,哪些既有行为会变、哪些依赖旧行为的应用会受影响,全部列出来再动工。把补丁拆成更小的原子改动逐个合入,出了问题时定位也会快得多。
CVE-2025-1094给PG生态所有衍生数据库提了个醒:安全补丁的合入不是终点,兼容性验证才是真正的落地环节。如果你也正在处理类似的补丁同步问题,希望这篇复盘能帮你少踩几个坑,至少在上线前把那四项检查先跑一遍。