两条转义规则打架,捅出 PostgreSQL 一个 9.8 的 SQL 注入
最近在跟踪 PHP 安全公告时,看到 CVE-2026-17543 这个漏洞。CVSS 9.8,CWE-89,一眼就是需要立刻处理的那种。但真正让我停下来琢磨的,是它的成因——不是某个函数写错了,而是两条转义规则各自都正确,组合在一起却产生了一个反直觉的漏洞。
两个"正确"的转义,拼出一个错误
问题出在 ext/pgsql 的五个函数上:pg_insert、pg_select、pg_update、pg_delete、pg_convert。对数组参数中的字符串值,它们统一交给 php_pgsql_convert 处理。这条链路上有两步:
PQescapeStringConn(libpq 提供)对字符串做转义;php_pgsql_add_quotes把转义后的结果用E'...'包裹成字符串常量。
拆开看,每一步都没毛病。
PQescapeStringConn 的标准语义是针对普通字符串 '...' 的:单引号翻倍(' → ''),反斜杠原样保留。因为普通字符串里反斜杠没有特殊含义。
但 php_pgsql_add_quotes 却用了 E'...'。在 PostgreSQL 里,E'...' 是 escape string,反斜杠是转义字符。
问题就在这:转义和包裹的语义不一致。
攻击者往字符串里插入一个 \'(反斜杠 + 单引号),PQescapeStringConn 会把单引号翻倍成 '',反斜杠则原样保留。于是拼进 E'...' 后,实际是 E'...\'...'。在 escape string 语境下,反斜杠把后面那个单引号转义掉了,第二个单引号才是字符串的闭合引号。原本只是字符串内容的 '',变成了"转义引号 + 提前闭合"。后续的恶意内容就逃逸成了原始 SQL 执行。
进一步说,standard_conforming_strings 在 PostgreSQL 9.1 之后默认开启。意思是默认配置下,'...' 严格按标准语义解析,反斜杠无特殊含义。这恰恰让 PQescapeStringConn 的"反斜杠原样保留"行为变得安全——可 E'...' 不走这个规则。
注入效果
-- 攻击者输入:foo\'; DROP TABLE bar; --
-- 拼进 E'...' 后
E'foo\''; DROP TABLE bar; --'
第一个单引号被反斜杠转义,字符串在 '' 处闭合,后面的内容以 SQL 代码执行。参数内容不再是参数内容,而是查询的一部分。
受影响版本与修复
- 受影响:PHP 8.2 < 8.2.33;8.3 < 8.3.33;8.4 < 8.4.24;8.5 < 8.5.9
- 修复版本:8.2.33、8.3.33、8.4.24、8.5.9
- 披露:GHSA-7qpv-r5mr-78m4
- 修复 commit:
ab048bd83b57
修复方案很直接:php_pgsql_add_quotes 不再用 E'...' 包裹,改用普通 '...'。这样反斜杠无特殊含义,单引号翻倍的语义就对了,PQescapeStringConn 的输出与包裹语义完全一致。
另外注意,pg_query_params 和 PDO_PGSQL 的预处理语句不受影响。它们走的是参数化路径,不拼接字符串,从根本上避开这个问题。
建议
- 升级到修复版本,这是最省事的。
- 如果短时间升不了,改用参数绑定:
pg_query_params、预处理语句、PDO 的 prepare/execute,任何方式都可以。 - 自查一下代码里有没有用
pg_insert/pg_select/pg_update/pg_delete直接拼接用户输入的场景。如果 PHP 版本在受影响范围内,且数据库连接走默认配置,这就是一个可被远程利用的注入点。
这次漏洞给的一个教训是:转义函数的安全假设必须和上下文匹配。 PQescapeStringConn 没有义务考虑调用方后面用 E'...' 包裹——它按普通字符串语义工作。库的职责是保持转义规则与包裹语义的一致性,而这次,两个"正确"的实现叠在一起,反而错了。