尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CVE-2025-1094修复翻车记:GaussDB合入PG补丁的隐患与规避

CVE-2025-1094修复翻车记:GaussDB合入PG补丁的隐患与规避 凌晨两点告警群里的消息把我从睡梦中拉起来某核心业务库的写入链路开始出现零星报错报错信息是“malformed record literal”和“invalid text representation”。第一反应不是业务变更而是最近为修复CVE-2025-1094漏洞、合入原生PostgreSQLPG那个PR时动了不该动的地方。CVE-2025-1094出在PG的libpq转义函数上攻击者可以通过精心构造的输入利用PQescapeLiteral这类函数在转义逻辑上的边界缺陷绕过单引号转义进而拼出恶意SQL形成SQL注入。GaussDB作为同源内核天然踩在同一个坑里所以安全团队给出的修复方案几乎一致跟进上游补丁。但“跟进”和“直接合入”是两回事。这次合入引发的问题比漏洞本身更让人头疼也让我结结实实上了一课数据库内核的补丁管理真的不能“拿来主义”。这篇文章不打算复述CVE公告我想把这次“合入原生PG的PR修复漏洞后翻车”的过程拆开讲清楚为什么会出问题、出了什么样的问题、一线怎么排查、后续怎么规避。给正在做数据库内核维护或负责GaussDB这类同源数据库安全加固的同行一个参考。1. 漏洞背景与合入决策为什么GaussDB必须跟进这个PR1.1 CVE-2025-1094的本质一个藏在转义函数里的注入点CVE-2025-1094的核心位置在libpq的转义函数上受影响的主要是PQescapeLiteral和PQescapeIdentifier。这两个函数的作用简单说就是把用户输入字符串里的特殊字符处理掉让它可以安全地作为SQL字面量或标识符拼进查询里。比如你写一个查询用户输入里带有单引号如果不转义单引号就可能提前结束字符串后面的内容就会被当成SQL语句执行。这个漏洞的微妙之处在于它对反斜杠的处理存在边界缺陷。PostgreSQL里有一个参数叫standard_conforming_strings默认开启时普通字符串里反斜杠不是转义符但如果你用E...这种转义字符串语法反斜杠就是转义符。问题就出在PQescapeLiteral内部构造返回结果时没有在所有需要的位置都正确加上反斜杠。安全研究者发现某些情况下一个以反斜杠结尾的输入可以在转义后的字符串里留下一个“逃逸”的单引号入口攻击者就能闭合字符串并注入额外SQL。上游PG社区的修复PR并不复杂核心就是在构造转义结果时统一补上缺失的反斜杠并增加相应的回归测试。对于跑在PG上的用户来说升级小版本或者打补丁就完事了。但对GaussDB来说事情远没有这么简单。GaussDB虽然和PG同源但经过了大量自研改造libpq的代码路径、编解码逻辑、甚至某些内部函数命名都做了定制不能直接把上游diff拿过来应用。1.2 同源内核的安全债GaussDB为什么绕不开上游补丁这里先解释一个很多人会问的问题PG的漏洞GaussDB为什么也受影响因为GaussDB的内核是从PostgreSQL分叉演进而来的虽然现在有大量自研特性但基础架构、SQL解析、执行器、libpq客户端协议这些底座仍然保留了PG的DNA。凡是PG公共代码库里的通用问题GaussDB基本都会命中只是受影响面的大小不同。安全团队在评估这个CVE时确认GaussDB的libpq同样存在可以被利用的路径。也就是说外部客户端通过libpq连接GaussDB时如果应用层把不可信输入交给PQescapeLiteral去转义攻击者就有机会注入SQL。这种情况下“等社区修复再跟进”是最常规的处置思路。于是团队决定把PG社区那个修复PR合入GaussDB代码分支。但这里埋了一个伏笔合入一个补丁不是执行一次git cherry-pick那么简单。GaussDB对libpq做过扩展比如支持了更多的认证协议、更严格的权限校验、甚至对某些字符串处理路径加了审计钩子。同一个PR里的diff落到PG上是几个hunk落到GaussDB上可能因为上下文对不上需要人肉调整。而人肉调整的过程恰恰是引入新问题的高发区。1.3 合入前的风险评估不能只把补丁当作一个diff我的观点是任何跨分支合入安全补丁都不能只看diff本身必须先回答三个问题。第一个问题是这个补丁依赖的上游上下文在我们分支里是否完整存在如果PR里修改了某个函数而这个函数在GaussDB里已经被重命名、拆分成多个函数那就要找出对应关系不能文件名对得上就硬合。第二个问题是补丁改变了什么行为而这些行为是否被我们自己的特性依赖比如PG社区可能认为某条转义路径永远不会出现某种字符但GaussDB的自研扩展里偏偏就可能出现合入后就会多走一个断言分支直接报错。第三个问题是补丁对应的回归测试能否在我们分支上跑通上游PR带的测试是基于PG的测试框架写的GaussDB有自己的一套expected文件跑不过去不代表补丁错了但需要花时间适配。适配过程中很容易把测试调整得“刚好通过”却掩盖了真实行为变化。这三个问题我们当时都过了一遍但显然还是低估了“行为变化”的影响范围。结果就是这个“严重隐患”在落地后以一种非常隐蔽的方式冒了出来。2. 合入原生PG补丁后最容易出现的四类严重隐患2.1 隐患一转义语义变化存量应用直接踩雷合入PR后的第一个隐患是转义后的结果和以前不一样了。理论上来说修复漏洞本身就是让转义结果更严格从安全角度是好事。但现实是很多存量应用已经习惯了过去的行为甚至在有意无意依赖它。举个例子在旧版本里PQescapeLiteral对某些字符的处理是“原样返回”的。比如输入一个不含单引号、不含反斜杠的普通字符串旧函数可能直接返回一个带引号的字符串而不做额外处理。修复后的版本无论输入是否包含特殊字符都会走统一的完整转义逻辑返回的内容里可能多出了反斜杠。对于直接把转义结果拼进SQL的应用来说多出来的反斜杠在某些数据库参数组合下会被解释成原样的反斜杠字符导致最终查询的语义改变。比如某条业务SQL依赖模板字符串的精确匹配转义后多出来的反斜杠就成了一个普通字符匹配不到数据报错或查不到结果。我当时排查时最头疼的就是应用报的错五花八门有报语法错误的有报找不到对象的还有逻辑上数据对不上的。等到定位到根因才发现是同一个问题转义结果变了。所以这个隐患排在第一位它不是崩溃式故障而是静默行为变更影响范围非常广。2.2 隐患二字符集与编码路径不一致触发隐藏断言第二个隐患出在字符集处理上。PG上游的PR在修复时默认场景是数据库编码为UTF-8、客户端编码也是UTF-8。但在GaussDB的实际部署环境里编码组合多种多样常见的有GBK、GB18030、LATIN1等。尤其是国产化替代场景中很多系统仍然沿用GBK编码的存量数据。上游补丁里新增了一个检查逻辑大意是在转义过程中逐个字节扫描输入遇到需要转义的字节就补反斜杠。这个逻辑在UTF-8下没有问题因为多字节字符的后续字节都有明确的取值范围0x80到0xBF不会和单引号、反斜杠这些ASCII字节混淆。但在GBK这类双字节字符集里一个汉字的第二个字节的取值范围是0x40到0xFE不含0x7F这个范围完全可能包含0x5C也就是反斜杠的ASCII码。如果补丁的扫描逻辑只是简单按字节判断就会把一个汉字的后半截误判成反斜杠强行插入一个转义符轻则导致字符乱码重则触发内部断言直接报错。这类问题在测试环境里极难暴露因为测试库通常是UTF-8。到了生产环境遇到GBK库就是大量报错。我们的排查记录里有一条典型报错是“invalid memory alloc request size”但实际上是转义函数处理非法编码序列时内部逻辑走到了意料之外的分支。后来我们用实际业务里的中文参数做最小复现发现转义后的字符串里汉字已经被拆得完全不可读。2.3 隐患三安全机制重叠修复补丁和自研防护互相打架第三个隐患和GaussDB的自研安全机制有关。GaussDB为了满足等保和金融合规要求在libpq链路上加了不少自己的防护逻辑比如SQL注入防护、访问控制、审计钩子。这些逻辑和上游PR修改的转义函数在同一个路径上执行。问题在于它们对“合法输入”的定义不一样。上游PR认为经过转义函数处理的字符串最终不会产生注入风险所以它的一些断言建立在“字符串一定安全”的基础上。而GaussDB自研的SQL注入防护模块会在转义之后重新扫描一遍发现字符串里出现了反斜杠和引号的组合就认为是可疑流量直接拦截。于是出现了非常搞笑的局面补丁修好了CVE漏洞但应用侧开始频繁出现“connection reset by peer”或“SQL injection detected”的告警。安全团队觉得防住了攻击业务团队觉得系统疯了。两边都没有错但合在一起就冲突了。这也说明合入上游补丁前必须把自研安全模块的检测规则和补丁的新行为对齐否则就是自己和自己打架。2.4 隐患四合入冲突处理不当把二次漏洞引入漏网路径最后一个隐患也是最严重的是合入冲突时处理不当导致修复只覆盖了一部分路径反而留下漏网之鱼。GaussDB的libpq里除了PQescapeLiteral和PQescapeIdentifier还有大量内部使用的字符串拼接函数比如在协议层构造查询时用到的appendStringLiteral、build_type_name等。上游PR只修改了公开API但GaussDB内部很多地方是绕过公开API直接调内部函数的。我们检查时发现合入过程中因为上下文冲突有一处diff被手动调整调整后适配到了某个非公开的转义路径但周边的相邻调用点没有跟着走完。结果就是外部API的转义是修复了但内部拼接SQL的某条私有路径仍然沿用旧逻辑攻击面没有被完全堵死。这等于说补丁让安全团队产生了“我们已经修复”的错觉但实际上漏洞依旧存在于某个更隐蔽的路径里。这也是为什么合入安全补丁不能以“diff能应用”为完成标志必须要做完整的路径覆盖分析。3. 一线排查实录从出现告警到定位根因的完整过程3.1 现场现象SQL报错、连接异常、性能回退我们这次生产环境出问题现象其实是分批出现的。最开始是某个业务库的慢查询数量突然上升但都是一些很小的查询单次执行时间从毫秒级涨到了几十毫秒。紧接着监控系统开始报一些奇怪的错误码集中在“22P02 invalid text representation”这类数据格式错误。再往后部分长连接开始被服务端主动断开业务侧重连后又恢复正常看起来就像网络抖动。这些现象单独看都不算严重但组合在一起就指向一个共同的方向libpq链路或服务器端字符串处理逻辑出了问题。当时团队里有人怀疑是数据库参数被动过有人怀疑是资源池不足但几个人同时想到了最近合入的CVE修复PR。因为故障的时间点正好在补丁合入并发布后不到48小时。3.2 排查步骤从日志到差异对比排查的第一步是打开客户端和服务端两边的日志捞报错上下文。客户端日志里能看到应用调用PQescapeLiteral的堆栈服务端日志里能看到执行器收到的SQL文本。两相对照发现服务端收到的SQL和客户端预期的SQL不一样多了一些反斜杠。比如应用期望的查询条件是name 张三服务端收到的却是name 张\三这在GBK编码下就是完全不同的两个值。第二步是做A/B验证。拿一个干净的GaussDB实例先回滚补丁再跑同样的业务SQL发现一切正常重新应用补丁后同样的SQL立刻报错。这个操作基本把问题锁定在补丁本身。第三步是差异对比。把PG社区的PR原版diff和GaussDB实际合入的diff逐行对比发现有几处hunk被人工调整过其中就包括对GBK编码场景的处理逻辑。再往后深挖确认问题就出在“按字节扫描输入”的修复算法对多字节编码不友好。3.3 根因确认用最小复现验证隐患为了不误伤补丁方向我做了最小复现。在测试库里执行一条带有中文参数值的简单查询客户端使用GBK编码调用PQescapeLiteral然后打印转义后的字符串。结果复现了生产现场转义后的字符串里中文汉字的最后一个字节被错误地当成了反斜杠处理插入了一个多余的转义符。这个最小复现案例后来成了内部bug单里的经典附件。它证明了一件事上游补丁本身是好的但GaussDB合入时必须对字符集场景做适配不能照搬。后来我们在此基础上又验证了LATIN1编码、SQL_ASCII编码下的行为发现都存在类似问题只是表现不同。整个排查过程大概花了三个小时。说快不快说慢不慢但如果没有“最近合入补丁”这个线索恐怕要绕很多弯路。这也提醒我数据库内核的故障尤其是补丁引入的故障最好先复位到“最近变化了什么”这个基准上。4. 规避与治理在GaussDB上安全合入PG安全补丁的操作清单4.1 合入前补丁分析、冲突评估、回归基线经过这次的教训我在团队内部推行了一份“PG安全补丁合入GaussDB操作清单”先讲合入前的三步。第一步是补丁分析不要只看PR描述要把PR的commit diff、关联issue、社区讨论帖都扒一遍。重点看两个地方一是补丁改了哪些函数这些函数在GaussDB源码里的对应物是什么二是补丁带了哪些测试用例这些用例覆盖了哪些编码和边界条件。把这些信息整理成一张映射表后面合入和验证都用得上。第二步是冲突评估提前用git cherry-pick模拟合入标注所有冲突点。对每个冲突点都要理解冲突的原因是因为GaussDB改了函数名还是改了函数内部结构还是新增了参数。理解原因后再决定怎么调整diff。最忌讳的是看到冲突就按直觉改上下文改完后没有回溯。第三步是回归基线在合入前先跑一遍GaussDB自有的libpq相关回归测试把通过的用例列表保存下来。合入后再跑一遍对比差异。新增失败的用例不代表补丁有问题但必须逐个解释原因绝对不允许直接把这批用例从基线里删掉。4.2 合入后针对性验证的十个检查点合入后的验证我给团队列了十个检查点核心思路就是“照着生产场景打一遍”。前三个检查点是功能面用UTF-8、GBK、GB18030、LATIN1四种编码分别测试PQescapeLiteral和PQescapeIdentifier确认中文、特殊符号、连续反斜杠、以反斜杠结尾的字符串都被正确处理。这一步能把绝大多数兼容性问题拦在测试环境里。中间三个检查点是安全面用CVE-2025-1094的原始PoC打一遍确认漏洞路径被封堵再额外构造几个变体比如在E...字符串里嵌套单引号和反斜杠确认不会出现漏网路径最后要检查内部私有转义路径确认没有绕过公开API的隐患。这里特别提醒安全验证不要只盯公开函数要把内部涉及字符串拼接的路径都过一遍。最后四个检查点是兼容面检查存量应用常用的几个典型SQL模板对比合入前后服务端收到的SQL是否一致检查自研安全模块的告警日志看有没有误报检查慢查询统计看有没有性能回退检查连接池的长连接稳定性看有没有异常断开。这四个检查点分别对应我们在生产环境踩到的四个坑任何一个都不能省。这十个检查点跑完心里才算有底。如果没有条件全部跑完至少要把编码相关的三个跑掉因为那是最容易踩坑的地方。4.3 出问题时的应急策略回滚、部分合入、二次修复如果真的出了问题应急策略要提前准备好不能临时拍脑袋。第一选择永远是整体回滚。数据库内核补丁不是业务功能没有“先上线再说”的余地。回滚到上一个稳定版本恢复业务后再慢慢分析。但整体回滚有个前提就是版本管理要干净一个发布分支只对应一个补丁否则回滚会连带撤掉其他变更。第二选择是部分合入。如果问题只出在特定编码路径可以考虑只回滚有问题的编码处理逻辑保留漏洞修复的其它部分。但部分合入很考验对代码的理解不建议没有完全定位根因时就做。第三选择是二次修复也就是在原补丁基础上继续打补丁修正错误逻辑。这个方案最省事但也最容易造成补丁越叠越乱。我的建议是二次修复必须写清楚和上游PR的关系最好把二次修复的diff单独提交方便以后跟随上游版本升级时统一处理。5. 常见问题与排查技巧实录我在实际处理过程中积累了一些高频问题的速查经验整理成表格供参考。问题现象可能根因排查建议转义后SQL里多出反斜杠补丁扫描逻辑未适配多字节编码用GBK编码跑最小复现用例报错invalid text representation服务端收到的SQL与期望不符对比客户端发送和服务端收到的SQL文本连接被对端重置自研安全模块误拦补丁新行为检查安全模块告警日志确认拦截特征长连接偶发断开服务端断言失败导致连接关闭查看服务端错误日志定位断言位置慢查询变多转义函数新增完整扫描路径用性能剖析工具比较函数调用耗时特定编码下乱码多字节字符被插入转义符测试GBK中文参数场景测试用例跑不过expected文件与PG不一致区分“补丁错误”和“测试适配问题”日志无异常但业务报错转义语义变化导致的静默行为变更对比补丁前后转义函数的输出差异排查技巧上想分享三条心得。第一条所有CVE修复类补丁合入后一定要顺手导出一份“补丁变更影响面清单”把修改的函数、涉及的调用点、可能影响的行为全部列出来故障定位时能节省大量时间。第二条遇到兼容性报错先看两次转义输出差异用diff命令对比处理前后的字符串比看日志更快。第三条不要轻易相信“回归测试全通过”回归测试只覆盖了测试库的编码和场景要主动补充生产环境的特有参数组合。6. 我的经验总结数据库内核补丁管理不能“拿来主义”6.1 管理层面补丁适配责任制这次踩坑本质上不是CVE-2025-1094补丁本身的问题而是我们在“合入”这个动作上过于乐观。上游PG的PR设计得再好也是为PG这个项目服务的。GaussDB有自己独有的编码场景、安全机制、内部调用路径这些差异决定了我们不能把“cherry-pick”等同于“修复完成”。我现在处理这类事情的原则是安全漏洞必须修但修的方式必须回到本分支的具体代码里重新走一遍。补丁不是终点验证才是终点。具体来说每个安全补丁合入时都要分配一名熟悉对应模块的工程师做“补丁适配人”这个人要顺着diff把上下游所有调用点捋一遍写一份适配说明再交给测试团队执行针对性的验证用例。这份适配说明要存档以后升级同源版本时还能复用。6.2 技术积累维护一张上游跟踪看板最后再分享一个小技巧给GaussDB这类同源但已深度定制内核维护一个“上游跟踪看板”每次PG社区发布安全公告第一时间登记对应CVE在GaussDB分支上的影响分析、补丁来源、合入状态、验证结论。这样下次再遇到类似CVE就能直接参考历史经验不用重新把流程走一遍。这个做法听着简单但真能救急。我这次如果不是因为之前积累了GBK编码相关的一个旧bug记录定位速度不会这么快。数据库内核维护拼的就是这些日常积累的细节。漏洞修复补丁合入这类工作尤其需要在流程上多留一道防线把“看起来没问题”变成“验证过没问题”。
返回列表