资讯详情

资讯详情

CVE-2025-1094:PostgreSQL转义绕过与psql元命令注入

PostgreSQL 最近有一个编号为 CVE-2025-1094 的漏洞补丁发出之后我在复盘排查时发现不少同行对这个漏洞的理解还停留在“知道名字但说不出细节”的程度。这个漏洞很不寻常它打破了一个很多人默认的安全假设——只要我们用了官方提供的转义函数把用户输入老老实实转义一遍SQL 注入就会离我们很远。但这一次转义函数自己先“失灵”了而且最终还能一路打到 psql 元命令执行危险程度直接拉满。这篇文章会把 CVE-2025-1094 从零开始讲透先补 SQL 注入和 psql 元命令的基础再拆漏洞的根因和触发链路最后给出自查方法、修复方案和防御加固建议。无论你是刚接触数据库的新手还是已经在生产环境里摸爬滚打多年的老手这篇都值得花 10 分钟认真过一遍因为这次的坑真的很容易踩。1. 先搞清楚这个漏洞到底有多大1.1 漏洞卡片CVE-2025-1094 基本信息先把最关键的档案信息列出来方便你转发给团队或者快速归档项目内容漏洞编号CVE-2025-1094受影响组件PostgreSQL 的 libpq 客户端库关键函数PQescapeLiteral()、PQescapeIdentifier()、PQescapeString()漏洞类型SQL 注入、转义绕过最高危害任意 SQL 注入、psql 元命令执行特定配置下可能到操作系统命令执行CVSS 评分8.4高危受影响版本17.0 ~ 17.2、16.0 ~ 16.6、15.0 ~ 15.10、14.0 ~ 14.15、13.0 ~ 13.18修复版本17.3、16.7、15.11、14.16、13.19这几个函数的定位是“官方钦定的转义函数”很多代码审计工具也会建议开发者用它们来处理动态 SQL。所以这次不是某个小众第三方组件的锅而是 PostgreSQL 自带客户端库核心函数出了问题影响面非常广。1.2 一句话概括这个漏洞的本质CVE-2025-1094 的核心问题可以压缩成一句话当输入中包含特定格式的无效字节序列时libpq 的转义函数在客户端看似正确转义了但数据到了服务端因为编码解码规则不一致某些字节会被服务端忽略或重组导致转义效果被抵消单引号等危险字符重新“裸奔”。这个“两端认知不一致”的漏洞模式比普通注入更阴险。以前我们教新人防注入会说“拼 SQL 时一定要用转义函数”这个经验本身没错但这次连转义函数都防不住对很多团队的信任体系是一次不小的冲击。2. 零基础补课SQL 注入和 psql 元命令到底是什么2.1 SQL 注入的本质把“数据”当“代码”执行用大白话讲SQL 注入就是在应用程序拼 SQL 语句时用户输入没有老老实实当数据而是被数据库当成 SQL 命令的一部分执行了。举个最简单的例子。你的程序写了一句SELECT * FROM users WHERE name 张三;如果这个“张三”是用户填的攻击者填成张三 OR 11拼接后就成了SELECT * FROM users WHERE name 张三 OR 11;这条语句会返回全表数据登录验证直接被打穿。更严重的攻击者还可以塞; DROP TABLE users; --这类操作。为了方便理解你可以把正常的 SQL 拼接想象成一份打印好的表格表格里的固定文字是“代码”用户填的内容是“数据”。注入攻击干的事情就是把用户输入里的特殊字符主要是单引号当作句法符号让数据库误以为“这段数据应该被当成代码处理”于是原本隔离的两层边界就被击穿了。2.2 psql 元命令从反斜杠开始的“快捷通道”psql 是 PostgreSQL 自带的命令行工具除了能执行普通 SQL还支持一批以反斜杠\开头的“元命令”。这些元命令不是 SQL 标准的一部分而是 psql 提供的交互快捷方式。常用的包括\set定义 psql 内部变量\gset把查询结果的列值存入 psql 变量\!在操作系统 shell 中执行一条命令\o把查询结果输出到文件\copy执行客户端侧的数据拷贝问题在于psql 元命令拥有“脱离 SQL 语法”的执行能力尤其是\!它直接调用操作系统命令。如果一个场景里攻击者构造的数据能渗透到 psql 变量再被元命令消费危害就会从“数据库被注入”升级到“数据库所在机器被控制”。CVE-2025-1094 的官方描述里明确提到了 psql 元命令执行意味着攻击链可以走得很深。2.3 转义函数存在的意义给输入穿上“防护服”开发者在写动态 SQL 时不能直接把用户输入塞进去否则就是给别人递刀子。转义函数的职责就是把输入字符串里的危险字符转成无害的“字面文本”。PQescapeLiteral() 是 libpq 提供的标准做法它会把字符串用单引号包裹起来并对内部的单引号、反斜杠等特殊字符做转义。比如用户输入abc OR 11 --PQescapeLiteral() 输出大致是abc OR 11 --也就是把单引号双写让数据库认为这是数据里的一个普通单引号而不是字符串边界。这层“防护服”在过去几十年是无数应用的安全底座也是为什么这次漏洞能引起这么大震动的原因——原来是铁布衫现在告诉你有几针没缝紧而且很难看出来。3. 漏洞根因转义逻辑被“无效字节”钻了空子3.1 两端编码规则不一致是绕过的关键CVE-2025-1094 的根因出在 libpq 的转义函数在处理“无效字节序列”时的逻辑缺陷。这里的无效字节序列指的是不符合当前字符集编码规则的字节组合。为了兼容历史上各种不规范的客户端PostgreSQL 服务端在解析某些无效多字节字符时并不会直接报错而是选择把无法识别的字节当作“零宽度”处理也就是干脆忽略掉或者把序列重新组合成另一个合法字符。问题就出在这里客户端 libpq 在转义时以字节为单位检查有没有、\等特殊字符服务端在解析时以“字符”为单位进行编码重组。两边的解析粒度不一致。如果攻击者构造一个特殊字节序列里面夹带单引号客户端转义时看到的是“普通字节单引号”会规规矩矩把单引号双写但服务端解析时那个特殊字节序列的“存在感”被抹掉单引号的语义就变了原本的双写单引号重新暴露成可以被利用的语法符号。这个感觉有点像加密传输两端用了不同的解密规则发送方加了密但接收方解密时某些字符被静默丢弃结果密文反而被还原成了明文。客户端以为自己在转义服务端却以为根本没转义两边对不上账危险就来了。3.2 从转义失败到注入的完整链路把整条攻击链路拆开看大致是四步攻击者构造一个包含特定无效字节序列的字符串里面夹带单引号、分号等 SQL 关键字符。业务代码调用 PQescapeLiteral() 或 PQescapeIdentifier() 对这个字符串做转义函数返回的结果从客户端视角看是“已经安全处理过”的。转义后的字符串进入 SQL 语句发送到服务端服务端在编码解码过程中按照自己的规则把无效字节剥离或重组导致原本双写的单引号恢复成可用的句法符号。攻击者输入中的 SQL 片段被服务端正常解析执行造成注入。注入成功后如果这个环境还能通过某个通道把数据交给 psql 元命令处理比如\gset把查询结果列值存入变量再在后续通过\set拼接构造新命令就可能进一步演变成元命令执行甚至操作系统命令执行。值得注意的是这个漏洞不仅影响应用代码中的动态 SQL也影响 psql 自身的一些交互场景。比如你从某个不可信数据源拿到一份 SQL 脚本文件用 psql 去执行它文件内容里可能含有精心构造的字节序列最终在\gset、\set这类元命令交互中被“激活”。3.3 为什么这个漏洞容易被忽略我在复盘时发现这个漏洞有几个“天生隐蔽”的属性。复现条件苛刻。它依赖特定的无效字节序列普通场景下很难碰到开发者日常测试很少会拿畸形编码去挑战转义函数所以线上即便存在风险也不容易暴露。静态代码扫描难以覆盖。像PQescapeLiteral()这种函数在代码里看起来就是标准的安全写法常规代码审计工具不会标记它们是风险点反而会认为“这里做了转义安全”。漏洞不在函数用没用上而在函数内部的边界情况处理这超出了大多数白盒扫描工具的能力范围。修复牵涉面广。libpq 是客户端库服务端和客户端都可能受影响。数据库服务器升级了还不够如果应用服务器用的还是旧版 libpq漏洞照样存在。这个问题在大型系统里尤其容易漏掉。4. 自查与修复三步确认你的环境是否安全4.1 第一步确认 PostgreSQL 版本升级到修复版本先别急着猜直接查一下版本SELECT version();如果版本落在受影响区间立刻规划升级。修复版本我已列在开头的表格里17.3、16.7、15.11、14.16、13.19对应的分支升级上去就能堵住漏洞。升级时的几个关键点要注意libpq是客户端库服务端升级不代表客户端自动安全。应用服务器、ETL 工具、脚本环境里装了什么版本的 libpq要一并排查使用pg_config --version或psql --version查看。升级前务必先做兼容性测试。这次修复涉及转义函数和编码处理逻辑极端情况下可能影响某些特殊的字符处理行为先在预发环境跑一遍完整回归。如果业务依赖某些老版本特有的行为升级后需要重点验证中文、表情符号等非 ASCII 字符的读写。提示版本检查不止看服务端客户端机器上的psql --version输出也要看。CVE-2025-1094 同时影响服务端和客户端两边都要达标。4.2 第二步扫描代码中的转义函数调用评估暴露面即使版本暂时无法立即升级也要先搞清楚自己的代码里到底哪些地方调用了这些函数。在项目目录里搜一下grep -rn PQescapeLiteral\|PQescapeIdentifier\|PQescapeString --include*.c --include*.cpp --include*.h .把搜出来的调用点逐个人工确认输入来源是谁是终端用户输入、API 参数、配置文件还是外部文件读取输入流经这些函数后拼到了 SQL 语句的什么位置有没有可能被二次读取、写入日志、存到 psql 变量再被元命令消费如果输入源完全受控那风险相对低如果输入源面向不可信用户且拼进 SQL那就属于高风险路径要在升级前临时加固。4.3 第三步收紧运行时权限减少“最后一击”的伤害万一被注入能做到什么程度很大程度上取决于数据库账号的权限。我在不少项目里见过用超级用户跑业务查询的情况一旦注入成功就是灾难。最小权限原则不只是一句口号具体落地至少要做到业务应用使用的数据库账号只授予业务所必需的库、表、函数权限撤销超级权限。禁止业务账号拥有COPY ... PROGRAM、CREATE EXTENSION这类高危权限尤其在无法确认客户端 libpq 版本时。开启数据库日志记录所有语句至少临时开启一段时间方便发现异常模式log_statement all log_line_prefix %m [%p] %q%u%d 日志会带来额外 IO 和存储开销生产环境可以只开一小段时间或者在低峰期开启等排查结束后改回更严格的配置。5. 升级之外的防御加固与避坑经验5.1 参数化查询才是最终解法这次漏洞最值得反思的一点是转义函数本身只是“第二道防线”参数化查询预处理语句才是根除 SQL 注入的根本办法。参数化查询把 SQL 结构固定下来用户输入只作为参数绑定不参与语句结构的解析根本不给“转义失效”留机会。拿 C 语言配合 libpq 举例不用字符串拼接 SQL而是用 PQprepare 加 PQexecPreparedPGresult *res PQprepare(conn, my_stmt, SELECT * FROM users WHERE name $1, 1, NULL); const char *paramValues[1] { user_input }; PGresult *res2 PQexecPrepared(conn, my_stmt, 1, paramValues, NULL, NULL, 0);这样写哪怕 user_input 里塞满无效字节序列、单引号和分号也只是一堆“值”绝不会被解释成 SQL 结构。很多高级语言驱动里的参数化查询也是同理Python 的 psycopg2 用%s占位符Java 的 JDBC 用?占位符效果一样。转义函数并没有失去存在价值但这次漏洞应该促使团队调整开发规范动态拼接 SQL 只能用于少量内部场景面向不可信输入的业务逻辑一律走参数化查询。5.2 升级过程中的兼容性避坑如果你现在已经在准备升级提醒几个我实际踩过的坑。第一个坑服务端升级后旧客户端连不上。PostgreSQL 的主版本升级通常要求客户端版本不低于某个基线如果一个业务集群里有的应用服务器长期没更新 libpq可能出现认证协议不兼容的问题。升级前把客户端的 libpq 版本一起纳入排期最好统一升级避免“服务端安全但客户端漏风”的尴尬局面。第二个坑转义行为差异。修复后的转义函数遇到无效字节序列可能会报错或拒绝处理。如果业务里有人拿一堆历史遗留的脏数据做入库升级后可能发现之前能跑通的流程突然报“invalid byte sequence”。这其实是一种“积极失败”是安全机制在起作用。把脏数据挡在门外总比让它们在库里埋雷强。第三个坑主从切换。PostgreSQL 高可用集群升级时要特别注意先升级备节点再切换主备关系否则一次无意的主从切换会把未修复的旧节点重新拉回主节点等于治标不治本。5.3 几个模拟排查案例帮你理解现象根据真实高频提问整理了几个模拟场景场景现象原因分析处理建议升级前业务一切正常升级后某条包含特殊编码的 SQL 报错 “invalid byte sequence for encoding UTF8”新版本对无效字节序列的容忍度降低主动报错拦截清理源头数据改用参数化绑定代码里用了 PQescapeLiteral注入仍然成功日志里出现异常 SQL 片段输入包含特定无效字节序列转义被服务端解码抵消升级 libpq改用参数化查询psql 执行脚本脚本被注入后执行了\!shell 命令脚本数据被元命令消费触发命令执行禁止执行来源不可信的 psql 脚本升级版本监控发现大量异常登录查询业务日志中单引号被反复双写但仍报语法错误有自动化扫描器正在探测端点但载荷未生效确认版本已修复检查访问控制启用日志记录这些场景我在不同的群里都见过类似讨论核心还是要回到版本和安全架构两个基本面版本修好参数化铺开心理上才会彻底踏实。5.4 长期防御的三个习惯最后聊点“结硬寨”的打法。单靠一个 CVE 的补丁防不住下一个未知漏洞。这次事件让我把几个习惯刻进了项目规范里第一个习惯构建独立的 SQL 模板层。所有 SQL 语句集中管理业务代码拿到的都是函数接口不是可拼接的字符串。这样审计起来非常轻松出问题时也能快速定位边界。第二个习惯把编码校验做在入口。既然问题出在无效字节序列那就在程序入口统一做字符集校验只接受合法 UTF-8连畸形数据进系统的机会都掐掉。可以用utf8proc或其他编码校验库也可以简单用语言自带的能力判断。第三个习惯订阅安全通告。PostgreSQL 官方发布安全通告时认真读一遍确认自己环境涉及的版本范围。很多时候补丁当天就出了但真正完成一轮全链路修复和验证可能要好几周。知道“哪里是弱点”比“知道要打补丁”更关键。按我个人经验每家公司都有几套数据库环境分布在不同的业务线上最大难点从来不是补丁包本身而是把“所有该升级的地方都找到”这件事做到位。这次 CVE-2025-1094 的教训就是别过分相信转义函数也别只盯着服务端版本客户端 libpq、psql 版本、应用代码路径全部都要纳入安全检查范围。最后再分享一个实战小技巧如果你不确定当前环境是否已经被扫描器盯上可以在备库临时开启一个不存在的账号观察是否有来源 IP 尝试登录并发送畸形 SQL。这类“蜜罐式”监测能帮你判断自己是否是攻击目标也能提前确认日志采集是否正常运转。安全这件事多做一步晚上就能少醒几次。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →