根本解法是使用参数化查询(如pg_query_params)并严格分离数据与SQL结构,动态表名/字段名须经白名单校验,禁用字符串拼接,辅以最小权限与日志审计。

直接拼接用户输入进 SELECT、WHERE 或 ORDER BY 子句,就是给攻击者递刀——PostgreSQL 不会区分“数据”和“代码”,只要字符串里混了 '、;、UNION 或 --,就可能执行任意语句。修复不是靠删掉几个字符,而是切断“输入→SQL执行”的通路。
用 pg_query_params() 替代 pg_query()
PHP + PostgreSQL 场景下最常见错误是把 $_GET['id'] 直接塞进查询字符串:
pg_query($conn, "SELECT * FROM users WHERE id = " . $_GET['id']);
这等于邀请攻击者输 1; DROP TABLE users;--。正确做法是交由 PostgreSQL 驱动处理参数绑定:
-
pg_query_params()会强制所有参数走二进制协议传输,数据库引擎根本不解析它们为 SQL 语法 - 占位符必须是
$1、$2这种位置式,不能用命名参数(如:id),PostgreSQL 原生不支持 - 数组值要展开成多个
$n,不能传整个数组进去——否则会触发类型转换失败或隐式字符串拼接
动态表名/字段名必须白名单校验
参数化查询只管“值”,不管“结构”。如果你要根据用户选择切换排序字段:ORDER BY $_GET['sort'],$1 在这里无效,因为 ORDER BY $1 会被当作字面量字符串而非列名。
可行方案只有两个:
- 硬编码白名单:
$allowed_sort = ['created_at', 'name', 'status'];,再用in_array($_GET['sort'], $allowed_sort)判断 - 映射表驱动:
$field_map = ['date' => 'created_at', 'title' => 'name'];,取$field_map[$_GET['sort']] ?? 'created_at' - 绝对不要用
pg_escape_identifier()处理用户输入的表名——它只转义引号,不防); DROP TABLE...这类注入
警惕 PostgreSQL 特有陷阱:大小写敏感 + LOWER() 包裹
Drupal CVE-2026-9082 就栽在这儿:为兼容 MySQL 的不区分大小写行为,框架自动给字段套 LOWER(),但构造占位符时又把用户传入的数组键名当 SQL 片段拼进去。结果 ['foo" -- ' => 'bar'] 直接撕开注释通道。
自查点:
- 检查所有
IN ()查询是否循环拼接了键名,而不是只处理值 - 确认 ORM 或查询构建器是否在 PostgreSQL 模式下启用
LOWER()自动包裹——如果不需要,关掉它 - 避免在条件生成逻辑里出现
"$key = $val"这类字符串模板,改用显式白名单 +pg_escape_identifier()(仅用于已知安全的标识符)
别信过滤,最小权限 + 日志审计才是兜底
正则过滤 union、sleep 等关键字纯属掩耳盗铃。攻击者早就能绕过:unIOn、SEL/**/ECT、Unicode 编码、甚至用 chr(117)||chr(110)||chr(105)||chr(111)||chr(110) 拼接关键词。
真正有效的防御层:
- 数据库连接账户只授予
SELECT、INSERT等必要权限,禁用CREATE、ALTER、EXECUTE - 开启
log_statement = 'mod',记录所有修改类语句,配合log_min_duration_statement = 1000捕获慢查询中的异常模式 - 对 JSON API 等无认证入口(如
/user/login?_format=json)做额外字段级校验,拒绝非预期键名
PostgreSQL 的 SQL 注入修复难点不在语法,而在信任边界的识别——哪部分是用户可控的、哪部分是开发可控的,必须划得清清楚楚。一旦混淆,$1 也救不了你。

















