必须用PQexecParams替代PQexec:SQL模板与参数严格分离,仅支持$1/$2位置占位符,需显式指定参数类型、长度及格式,杜绝SQL注入。

libpq里怎么用PQexecParams而不是PQexec
直接拼接字符串调用 PQexec 是最常见也最危险的写法。libpq 提供了真正安全的替代方案:必须改用 PQexecParams,它把 SQL 模板和参数值完全分离。
关键不是“能不能用”,而是“不用就会被注入”。PostgreSQL 服务端在收到 PQexecParams 请求时,会跳过语法重解析,只绑定并执行已缓存的执行计划——恶意输入连进解析器的机会都没有。
-
PQexec接收完整 SQL 字符串,用户输入一旦混入,立刻变成可执行代码 -
PQexecParams第一个参数是纯 SQL 模板(含$1,$2占位符),后续所有参数都走二进制或严格转义通道 - 即使参数值里包含
;,--,OR 1=1或pg_sleep(10),也不会改变语句结构
占位符写法必须用 $1 $2,不能用 ? 或 %s
PostgreSQL 的协议层不识别 ? 或 Python/PHP 风格的 %s。libpq 只认位置式占位符 $1, $2…… 这是硬性约束,不是风格偏好。
写成 WHERE id = ? 或 WHERE name = '%s' 会导致 PostgreSQL 报错 ERROR: syntax error at or near "?",因为服务端根本不知道这是占位符还是字面量。
- 正确模板:
"SELECT * FROM users WHERE status = $1 AND created_at > $2" - 错误模板:
"SELECT * FROM users WHERE status = ?"(服务端报错) - 错误模板:
"SELECT * FROM users WHERE name = '%s'"(等同于字符串拼接,毫无防护)
参数类型和长度必须显式传给 PQexecParams
PQexecParams 要求你明确告诉 PostgreSQL 每个参数的 OID 类型、长度和是否为 NULL。这看起来繁琐,但恰恰是安全的关键一环——类型强约束让服务端拒绝非法转换。
比如传一个字符串参数,你得填 paramTypes[0] = TEXTOID;传整数就得用 INT4OID。如果传错类型,PostgreSQL 直接拒绝执行,而不是尝试隐式转换(那可能被绕过)。
- 参数数组
const char * const * paramValues存的是 C 字符串指针,不能是 NULL 指针,空值要用paramLengths[i] = 0+paramFormats[i] = 0+ 单独设paramIsNULL[i] = 1 - 长度数组
const int * paramLengths必须对应每个参数的真实字节长度(不是 strlen,注意多字节字符) - 格式数组
const int * paramFormats:0 表示文本格式,1 表示二进制格式;文本格式更常用,但二进制能避免编码歧义
别漏掉 PQescapeStringConn,它只救得了旧代码
如果你暂时没法重构到 PQexecParams,而必须用 PQexec,那就只能靠 PQescapeStringConn 做最后一道防线。但它只是转义单引号、反斜杠等,对堆叠注入(;)、注释符(--)、布尔盲注(AND 1=1)完全无效。
更麻烦的是:这个函数需要连接对象作为参数,且返回的是新分配的内存,调用者必须 free()。漏掉 free 就是内存泄漏;传错连接对象就可能转义失败。
- 仅适用于临时补丁,不能当长期方案
- 对 JSON 字段、LIKE 模式中的通配符(
%,_)需额外处理,PQescapeStringConn不管这些 - 一旦参数含二进制数据(如图片 base64 片段),转义会破坏原始字节,必须改用
PQexecParams
真正难的不是写对一行 PQexecParams 调用,而是确保整个项目里没有漏掉任何一个动态拼接点——尤其是日志查询、导出条件、管理后台的模糊搜索这类“非核心业务”逻辑,最容易被忽略。











